From 29c7b9308e977e93611a528be58ec201c7ab5f85 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 19 Dec 2025 13:18:18 -0600 Subject: [PATCH 01/90] rough cascading compaction impl Improvements and bugfixes Fix sompaction status after rebasing Fix missing import after rebase fix checkstyle issues fill out javadocs address claude code review comments Add isReady concept to compaction rule provider and gate task creation on provider being ready Fix an issue in AbstractRuleProvider when it comes to variable length periods like month and year Implement a composing rule provider for chaining multiple rule providers --- .../compact/CompactionSupervisorTest.java | 115 +++++ .../apache/druid/guice/SupervisorModule.java | 4 +- .../compact/CascadingCompactionTemplate.java | 474 ++++++++++++++++++ .../CompactionConfigBasedJobTemplate.java | 15 +- .../compact/CompactionConfigFinalizer.java | 55 ++ .../compact/CompactionSupervisorSpec.java | 6 +- .../compaction/AbstractCompactionRule.java | 157 ++++++ .../compaction/CompactionDimensionsRule.java | 86 ++++ .../compaction/CompactionFilterRule.java | 87 ++++ .../compaction/CompactionGranularityRule.java | 84 ++++ .../compaction/CompactionIOConfigRule.java | 78 +++ .../compaction/CompactionMetricsRule.java | 85 ++++ .../compaction/CompactionProjectionRule.java | 89 ++++ .../server/compaction/CompactionRule.java | 84 ++++ .../compaction/CompactionRuleProvider.java | 200 ++++++++ .../server/compaction/CompactionStatus.java | 116 ++++- .../CompactionTuningConfigRule.java | 87 ++++ .../ComposingCompactionRuleProvider.java | 306 +++++++++++ .../InlineCompactionRuleProvider.java | 390 ++++++++++++++ ...nlineSchemaDataSourceCompactionConfig.java | 23 + .../CompactionDimensionsRuleTest.java | 172 +++++++ .../compaction/CompactionFilterRuleTest.java | 331 ++++++++++++ .../CompactionGranularityRuleTest.java | 190 +++++++ .../CompactionIOConfigRuleTest.java | 172 +++++++ .../compaction/CompactionMetricsRuleTest.java | 178 +++++++ .../CompactionProjectionRuleTest.java | 168 +++++++ .../compaction/CompactionStatusTest.java | 307 ++++++++++++ .../CompactionTuningConfigRuleTest.java | 196 ++++++++ .../ComposingCompactionRuleProviderTest.java | 418 +++++++++++++++ .../InlineCompactionRuleProviderTest.java | 308 ++++++++++++ 30 files changed, 4977 insertions(+), 4 deletions(-) create mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java create mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 5355f1601ae4..bc1158c3e858 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -25,8 +25,11 @@ import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; import org.apache.druid.indexing.common.task.IndexTask; +import org.apache.druid.indexing.compact.CascadingCompactionTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.Segments; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.IAE; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; @@ -35,6 +38,10 @@ import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.server.compaction.CompactionGranularityRule; +import org.apache.druid.server.compaction.CompactionStatus; +import org.apache.druid.server.compaction.CompactionTuningConfigRule; +import org.apache.druid.server.compaction.InlineCompactionRuleProvider; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; @@ -51,11 +58,15 @@ import org.apache.druid.testing.embedded.junit5.EmbeddedClusterTestBase; import org.hamcrest.Matcher; import org.hamcrest.Matchers; +import org.joda.time.DateTime; +import org.joda.time.Interval; import org.joda.time.Period; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -268,6 +279,110 @@ public void test_compaction_withPersistLastCompactionStateFalse_storesOnlyFinger verifySegmentsHaveNullLastCompactionStateAndNonNullFingerprint(); } + @MethodSource("getEngine") + @ParameterizedTest(name = "compactionEngine={0}") + public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompactionRules(CompactionEngine compactionEngine) + { + configureCompaction(compactionEngine); + + DateTime now = DateTimes.nowUtc(); + + // Note that we are purposely creating events in intervals like this to make the test deterministic regardless of when it is run. + // The supervisor will use the current time as reference time to determine which rules apply to which segments so we take extra + // care to create segments that fall cleanly into the different rule periods that we are testing. + String freshEvents = generateEventsInInterval( + new Interval(now.minusHours(4), now), + 4, + Duration.ofMinutes(30).toMillis() + ); + String hourRuleEvents = generateEventsInInterval( + new Interval(now.minusDays(3), now.minusDays(2)), + 5, + Duration.ofMinutes(90).toMillis() + ); + String dayRuleEvents = generateEventsInInterval( + new Interval(now.minusDays(31), now.minusDays(14)), + 7, + Duration.ofHours(25).toMillis() + ); + + String allData = freshEvents + "\n" + hourRuleEvents + "\n" + dayRuleEvents; + + runIngestionAtGranularity( + "FIFTEEN_MINUTE", + allData + ); + Assertions.assertEquals(16, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); + + CompactionGranularityRule hourRule = new CompactionGranularityRule( + "hourRule", + "Compact to HOUR granularity for data older than 1 days", + Period.days(1), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + CompactionGranularityRule dayRule = new CompactionGranularityRule( + "dayRule", + "Compact to DAY granularity for data older than 2 days", + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ); + + CompactionTuningConfigRule tuningConfigRule = new CompactionTuningConfigRule( + "tuningConfigRule", + "Use dimension range partitioning with max 1000 rows per segment", + Period.days(1), + new UserCompactionTaskQueryTuningConfig( + null, + null, + null, + null, + null, + new DimensionRangePartitionsSpec(1000, null, List.of("item"), false), + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ) + ); + + InlineCompactionRuleProvider ruleProvider = InlineCompactionRuleProvider.builder() + .granularityRules(List.of(hourRule, dayRule)) + .tuningConfigRules(List.of(tuningConfigRule)) + .build(); + + CascadingCompactionTemplate cascadingCompactionTemplate = new CascadingCompactionTemplate(dataSource, ruleProvider); + runCompactionWithSpec(cascadingCompactionTemplate); + waitForAllCompactionTasksToFinish(); + + Assertions.assertEquals(4, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); + Assertions.assertEquals(5, getNumSegmentsWith(Granularities.HOUR)); + Assertions.assertEquals(7, getNumSegmentsWith(Granularities.DAY)); + } + + private String generateEventsInInterval(Interval interval, int numEvents, long spacingMillis) + { + List events = new ArrayList<>(); + + for (int i = 1; i <= numEvents; i++) { + DateTime eventTime = interval.getStart().plus(spacingMillis * i); + if (eventTime.isAfter(interval.getEnd())) { + throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); + } + events.add(eventTime + ",item" + i + "," + (100 + i * 5)); + } + + return String.join("\n", events); + } + private void verifySegmentsHaveNullLastCompactionStateAndNonNullFingerprint() { overlord diff --git a/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java b/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java index 73e8e06e8964..03124e98e02f 100644 --- a/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java +++ b/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.google.common.collect.ImmutableList; import com.google.inject.Binder; +import org.apache.druid.indexing.compact.CascadingCompactionTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.supervisor.SupervisorStateManagerConfig; import org.apache.druid.indexing.scheduledbatch.ScheduledBatchSupervisorSpec; @@ -47,7 +48,8 @@ public List getJacksonModules() new SimpleModule(getClass().getSimpleName()) .registerSubtypes( new NamedType(CompactionSupervisorSpec.class, CompactionSupervisorSpec.TYPE), - new NamedType(ScheduledBatchSupervisorSpec.class, ScheduledBatchSupervisorSpec.TYPE) + new NamedType(ScheduledBatchSupervisorSpec.class, ScheduledBatchSupervisorSpec.TYPE), + new NamedType(CascadingCompactionTemplate.class, CascadingCompactionTemplate.TYPE) ) ); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java new file mode 100644 index 000000000000..e82c66712322 --- /dev/null +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java @@ -0,0 +1,474 @@ +/* + * 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.indexing.compact; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.apache.druid.indexer.CompactionEngine; +import org.apache.druid.indexing.input.DruidInputSource; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.server.compaction.CompactionDimensionsRule; +import org.apache.druid.server.compaction.CompactionFilterRule; +import org.apache.druid.server.compaction.CompactionGranularityRule; +import org.apache.druid.server.compaction.CompactionIOConfigRule; +import org.apache.druid.server.compaction.CompactionMetricsRule; +import org.apache.druid.server.compaction.CompactionProjectionRule; +import org.apache.druid.server.compaction.CompactionRuleProvider; +import org.apache.druid.server.compaction.CompactionStatus; +import org.apache.druid.server.compaction.CompactionTuningConfigRule; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Template to perform period-based cascading compaction. Contains a list of + * {@link org.apache.druid.server.compaction.CompactionRule} which divide the segment timeline into compactible + * intervals. Each rule specifies a period relative to the current time which is + * used to determine its applicable interval: + * + * + * If two adjacent rules explicitly specify a segment granularity, the boundary + * between them may be adjusted to ensure that there are no uncompacted gaps in the timeline. + *

+ * This template never needs to be deserialized as a {@code BatchIndexingJobTemplate} + */ +public class CascadingCompactionTemplate implements CompactionJobTemplate, DataSourceCompactionConfig +{ + private static final Logger LOG = new Logger(CascadingCompactionTemplate.class); + + public static final String TYPE = "compactCascade"; + + private final String dataSource; + private final CompactionRuleProvider ruleProvider; + + @JsonCreator + public CascadingCompactionTemplate( + @JsonProperty("dataSource") String dataSource, + @JsonProperty("ruleProvider") CompactionRuleProvider ruleProvider + ) + { + this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); + this.ruleProvider = Objects.requireNonNull(ruleProvider, "'ruleProvider' cannot be null"); + } + + @Override + @JsonProperty + public String getDataSource() + { + return dataSource; + } + + /** + * Creates a config finalizer that optimizes filter rules for cascading compaction. + * When a candidate segment has already been compacted with a subset of filter rules, + * this finalizer computes the minimal set of additional filter rules needed. + * This optimization reduces bitmap operations during compaction. + */ + private static CompactionConfigFinalizer createCascadingFinalizer() + { + return (config, candidate, params) -> { + // Only optimize if candidate has been compacted before and config has a NotDimFilter + if (candidate.getCurrentStatus() != null && + !candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON) && + config.getTransformSpec() != null && + config.getTransformSpec().getFilter() != null && + config.getTransformSpec().getFilter() instanceof NotDimFilter) { + + // Compute the minimal set of filter rules needed for this candidate + NotDimFilter reducedTransformSpecFilter = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + (NotDimFilter) config.getTransformSpec().getFilter(), + params.getCompactionStateCache() + ); + + // Safe cast: we know this is InlineSchemaDataSourceCompactionConfig because we just built it + return ((InlineSchemaDataSourceCompactionConfig) config).toBuilder() + .withTransformSpec(new CompactionTransformSpec(reducedTransformSpecFilter)) + .build(); + } + + return config; // No optimization needed, return original config + }; + } + + @Override + public List createCompactionJobs( + DruidInputSource source, + CompactionJobParams jobParams + ) + { + // Check if the rule provider is ready before attempting to create jobs + if (!ruleProvider.isReady()) { + LOG.info( + "Rule provider [%s] is not ready, skipping compaction job creation for dataSource[%s]", + ruleProvider.getType(), + dataSource + ); + return Collections.emptyList(); + } + + final List allJobs = new ArrayList<>(); + final DateTime currentTime = jobParams.getScheduleStartTime(); + + List sortedPeriods = ruleProvider.getCondensedAndSortedPeriods(currentTime); + if (sortedPeriods == null || sortedPeriods.isEmpty()) { + return Collections.emptyList(); + } + + // Generate intervals from periods and create jobs for each + List intervals = generateIntervalsFromPeriods(sortedPeriods, currentTime, ruleProvider.getGranularityRules()); + for (Interval compactionInterval : intervals) { + InlineSchemaDataSourceCompactionConfig.Builder builder = InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource(dataSource) + .withSkipOffsetFromLatest(Period.ZERO); + + // Apply all applicable compaction rules to the builder + int ruleCount = applyRulesToBuilder(builder, compactionInterval, currentTime); + + if (ruleCount > 0) { + LOG.info("Creating compaction jobs for interval[%s] with %d rules", compactionInterval, ruleCount); + allJobs.addAll( + createJobsForSearchInterval( + new CompactionConfigBasedJobTemplate(builder.build(), createCascadingFinalizer()), + compactionInterval, + source, + jobParams + ) + ); + } else { + LOG.info("No applicable compaction rules found for interval[%s]", compactionInterval); + } + } + return allJobs; + } + + /** + * Generates cascading intervals from sorted periods. + *

+ * For periods [P7D, P30D, P90D], generates intervals: + *

+ *

+ * If adjacent rules have segment granularities defined, boundaries are adjusted + * to align with granularity buckets to prevent gaps in the timeline. Example: + *

+   * Given P7D rule with HOUR granularity and P30D rule with DAY granularity,
+   * and referenceTime = 2025-12-19 14:37:22:
+   *
+   * Without adjustment:
+   *   Calculated boundary: 2025-11-19 14:37:22 (now - 30 days)
+   *
+   * With adjustment:
+   *   Aligned boundary: 2025-11-20 00:00:00 (aligned to start of next day)
+   *
+   * This ensures the DAY-granularity rule creates complete day-aligned segments
+   * and the HOUR-granularity rule starts cleanly at a day boundary.
+   * 
+ */ + private List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) + { + List intervals = new ArrayList<>(); + DateTime previousAdjustedBoundary = null; + + for (int i = 0; i < sortedPeriods.size(); i++) { + // End is either the previous adjusted boundary, or the raw calculation for the first interval + DateTime end = previousAdjustedBoundary != null + ? previousAdjustedBoundary + : referenceTime.minus(sortedPeriods.get(i)); + DateTime start; + + if (i + 1 < sortedPeriods.size()) { + // Bounded interval: between two periods + // We may need to adjust the start time to avoid gaps if both adjacent rules have segment granularities defined. + final DateTime calculatedStartTime = referenceTime.minus(sortedPeriods.get(i + 1)); + final int finalI = i; + CompactionGranularityRule currentRule = granularityRules.stream() + .filter(rule -> + rule.getPeriod().equals(sortedPeriods.get(finalI)) && rule.getGranularityConfig().getSegmentGranularity() != null) + .findFirst() + .orElse(null); + CompactionGranularityRule beforeRule = granularityRules.stream() + .filter(rule -> + rule.getPeriod().equals(sortedPeriods.get(finalI + 1)) && rule.getGranularityConfig().getSegmentGranularity() != null) + .findFirst() + .orElse(null); + + if (currentRule == null || beforeRule == null) { + start = calculatedStartTime; + } else { + final Granularity granularity = currentRule.getGranularityConfig().getSegmentGranularity(); + final Granularity beforeGranularity = beforeRule.getGranularityConfig().getSegmentGranularity(); + + final DateTime beforeRuleEffectiveEnd = beforeGranularity.bucketStart(calculatedStartTime); + final DateTime possibleStartTime = granularity.bucketStart(beforeRuleEffectiveEnd); + start = possibleStartTime.isBefore(beforeRuleEffectiveEnd) + ? granularity.increment(possibleStartTime) + : possibleStartTime; + } + + // Save the adjusted start boundary for the next (older) interval's end + previousAdjustedBoundary = start; + } else { + // Unbounded interval: from the earliest time. This allows the last rule to cover all remaining segments + // into the past + start = DateTimes.MIN; + } + + intervals.add(new Interval(start, end)); + } + return intervals; + } + + /** + * Applies all applicable compaction rules to the builder for the given interval. + * + * @return number of rules applied + */ + private int applyRulesToBuilder( + InlineSchemaDataSourceCompactionConfig.Builder builder, + Interval compactionInterval, + DateTime referenceTime + ) + { + int ruleCount = 0; + + // Granularity rules (non-additive, take first) + List granularityRules = ruleProvider.getGranularityRules(compactionInterval, referenceTime); + if (!granularityRules.isEmpty()) { + LOG.info("Applying granularity rule %s for interval %s", granularityRules.get(0).getId(), compactionInterval); + builder.withGranularitySpec(granularityRules.get(0).getGranularityConfig()); + ruleCount += 1; + } + + // Tuning config rules (non-additive, take first) + List tuningConfigRules = ruleProvider.getTuningConfigRules(compactionInterval, referenceTime); + if (!tuningConfigRules.isEmpty()) { + LOG.info("Applying tuning config rule %s for interval %s", tuningConfigRules.get(0).getId(), compactionInterval); + builder.withTuningConfig(tuningConfigRules.get(0).getTuningConfig()); + ruleCount += 1; + } + + // Metrics rules (non-additive, take first) + List metricsRules = ruleProvider.getMetricsRules(compactionInterval, referenceTime); + if (!metricsRules.isEmpty()) { + LOG.info("Applying metrics rule %s for interval %s", metricsRules.get(0).getId(), compactionInterval); + builder.withMetricsSpec(metricsRules.get(0).getMetricsSpec()); + ruleCount += 1; + } + + // Dimensions rules (non-additive, take first) + List dimensionsRules = ruleProvider.getDimensionsRules(compactionInterval, referenceTime); + if (!dimensionsRules.isEmpty()) { + LOG.info("Applying dimensions rule %s for interval %s", dimensionsRules.get(0).getId(), compactionInterval); + builder.withDimensionsSpec(dimensionsRules.get(0).getDimensionsSpec()); + ruleCount += 1; + } + + // IO config rules (non-additive, take first) + List ioConfigRules = ruleProvider.getIOConfigRules(compactionInterval, referenceTime); + if (!ioConfigRules.isEmpty()) { + LOG.info("Applying IO config rule %s for interval %s", ioConfigRules.get(0).getId(), compactionInterval); + builder.withIoConfig(ioConfigRules.get(0).getIoConfig()); + ruleCount += 1; + } + + // Projection rules (additive, combine all) + List projectionRules = ruleProvider.getProjectionRules(compactionInterval, referenceTime); + if (!projectionRules.isEmpty()) { + LOG.info("Applying [%d] projection rules for interval %s", projectionRules.size(), compactionInterval); + builder.withProjections( + projectionRules.stream() + .flatMap(rule -> rule.getProjections().stream()) + .collect(Collectors.toList()) + ); + ruleCount += projectionRules.size(); + } + + // Filter rules (additive, combine with OR and wrap in NOT) + List filterRules = ruleProvider.getFilterRules(compactionInterval, referenceTime); + if (!filterRules.isEmpty()) { + LOG.info("Applying up to [%d] filter rules for interval %s", filterRules.size(), compactionInterval); + List removeConditions = filterRules.stream() + .map(CompactionFilterRule::getFilter) + .collect(Collectors.toList()); + + DimFilter removeFilter = removeConditions.size() == 1 + ? removeConditions.get(0) + : new OrDimFilter(removeConditions); + DimFilter finalFilter = new NotDimFilter(removeFilter); + builder.withTransformSpec(new CompactionTransformSpec(finalFilter)); + ruleCount += filterRules.size(); + } + + return ruleCount; + } + + private List createJobsForSearchInterval( + CompactionJobTemplate template, + Interval searchInterval, + DruidInputSource inputSource, + CompactionJobParams jobParams + ) + { + return template.createCompactionJobs( + inputSource.withInterval(searchInterval), + jobParams + ); + } + + @Override + public String getType() + { + return TYPE; + } + + // Legacy fields from DataSourceCompactionConfig that are not used by this template + + @Nullable + @Override + public CompactionEngine getEngine() + { + return null; + } + + @Override + public int getTaskPriority() + { + return 0; + } + + @Override + public long getInputSegmentSizeBytes() + { + return 0; + } + + @Nullable + @Override + public Integer getMaxRowsPerSegment() + { + return 0; + } + + @Override + public Period getSkipOffsetFromLatest() + { + return null; + } + + @Nullable + @Override + public UserCompactionTaskQueryTuningConfig getTuningConfig() + { + return null; + } + + @Nullable + @Override + public UserCompactionTaskIOConfig getIoConfig() + { + return null; + } + + @Nullable + @Override + public Map getTaskContext() + { + return Map.of(); + } + + @Nullable + @Override + public Granularity getSegmentGranularity() + { + return null; + } + + @Nullable + @Override + public UserCompactionTaskGranularityConfig getGranularitySpec() + { + return null; + } + + @Nullable + @Override + public List getProjections() + { + return List.of(); + } + + @Nullable + @Override + public CompactionTransformSpec getTransformSpec() + { + return null; + } + + @Nullable + @Override + public UserCompactionTaskDimensionsConfig getDimensionsSpec() + { + return null; + } + + @Nullable + @Override + public AggregatorFactory[] getMetricsSpec() + { + return new AggregatorFactory[0]; + } + + @JsonProperty + private CompactionRuleProvider getRuleProvider() + { + return ruleProvider; + } +} diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java index 24b6e1f6af40..5089fcd270d0 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java @@ -48,10 +48,20 @@ public class CompactionConfigBasedJobTemplate implements CompactionJobTemplate { private final DataSourceCompactionConfig config; + private final CompactionConfigFinalizer configFinalizer; public CompactionConfigBasedJobTemplate(DataSourceCompactionConfig config) + { + this(config, CompactionConfigFinalizer.IDENTITY); + } + + public CompactionConfigBasedJobTemplate( + DataSourceCompactionConfig config, + CompactionConfigFinalizer configFinalizer + ) { this.config = config; + this.configFinalizer = configFinalizer; } @Nullable @@ -82,9 +92,12 @@ public List createCompactionJobs( while (segmentIterator.hasNext()) { final CompactionCandidate candidate = segmentIterator.next(); + // Allow template-specific customization of the config per candidate + DataSourceCompactionConfig finalConfig = configFinalizer.finalizeConfig(config, candidate, params); + ClientCompactionTaskQuery taskPayload = CompactSegments.createCompactionTask( candidate, - config, + finalConfig, params.getClusterCompactionConfig().getEngine(), indexingStateFingerprint, params.getClusterCompactionConfig().isStoreCompactionStatePerSegment() diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java new file mode 100644 index 000000000000..bc0b7bca087f --- /dev/null +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java @@ -0,0 +1,55 @@ +/* + * 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.indexing.compact; + +import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; + +/** + * Functional interface for customizing a {@link DataSourceCompactionConfig} for a specific + * {@link CompactionCandidate} before creating a compaction job. This allows template-specific + * logic to be injected without hardcoding behavior in {@link CompactionConfigBasedJobTemplate}. + *

+ * For example, cascading compaction templates can use this to optimize filter rules based on + * the candidate's compaction state, while simpler templates can use the identity finalizer. + */ +@FunctionalInterface +public interface CompactionConfigFinalizer +{ + /** + * Customize the compaction config for a specific candidate. + * + * @param config the base compaction config + * @param candidate the segment candidate being compacted + * @param params the compaction job parameters + * @return the finalized config to use for this candidate (may be the same as input or a modified version) + */ + DataSourceCompactionConfig finalizeConfig( + DataSourceCompactionConfig config, + CompactionCandidate candidate, + CompactionJobParams params + ); + + /** + * Identity finalizer that returns the config unchanged. + * Use this for templates that don't need per-candidate customization. + */ + CompactionConfigFinalizer IDENTITY = (config, candidate, params) -> config; +} diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java index cf12be8ce90e..998d7e0c7e67 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java @@ -98,7 +98,11 @@ public CompactionSupervisor createSupervisor() */ public CompactionJobTemplate getTemplate() { - return new CompactionConfigBasedJobTemplate(spec); + if (spec instanceof CascadingCompactionTemplate) { + return (CascadingCompactionTemplate) spec; + } else { + return new CompactionConfigBasedJobTemplate(spec); + } } @Override diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java new file mode 100644 index 000000000000..4e9ff2a44f1a --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java @@ -0,0 +1,157 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.logger.Logger; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * Base implementation for compaction rules that apply based on data age thresholds. + *

+ * Provides period-based applicability logic: a rule with period P7D applies to data + * older than 7 days. Subclasses define specific compaction configuration (granularity, + * filters, tuning, etc.) and whether multiple rules can combine (additive vs non-additive). + *

+ * The {@link #appliesTo(Interval, DateTime)} method determines if an interval is fully, + * partially, or not covered by this rule's threshold, enabling cascading compaction + * strategies where different rules apply to different age tiers of data. + */ +public abstract class AbstractCompactionRule implements CompactionRule +{ + private static final Logger LOG = new Logger(AbstractCompactionRule.class); + + private final String id; + private final String description; + private final Period period; + + public AbstractCompactionRule( + @Nonnull String id, + @Nullable String description, + @Nonnull Period period + ) + { + this.id = Objects.requireNonNull(id, "id cannot be null"); + this.description = description; + this.period = Objects.requireNonNull(period, "period cannot be null"); + + validatePeriodIsPositive(period); + } + + /** + * Validates that a period represents a positive duration. + *

+ * For periods with precise units (days, hours, minutes, seconds), validates by converting + * to a standard duration. For periods with variable-length units (months, years), validates + * that at least one component is positive, since these cannot be converted to a precise duration. + * + * @param period the period to validate + * @throws IllegalArgumentException if the period is not positive + */ + private static void validatePeriodIsPositive(Period period) + { + try { + // Try converting to standard duration for precise validation + if (period.toStandardDuration().getMillis() <= 0) { + throw new IllegalArgumentException("period must be positive, got: " + period); + } + } + catch (UnsupportedOperationException e) { + // Period contains months or years which have variable length + // Validate that at least one component is positive + if (!isPeriodPositive(period)) { + throw new IllegalArgumentException( + "period must be positive (contains months/years but all components are non-positive), got: " + period + ); + } + } + } + + /** + * Checks if a period with variable-length components (months/years) is positive. + * + * @param period the period to check + * @return true if any component is positive + */ + private static boolean isPeriodPositive(Period period) + { + return period.getYears() > 0 + || period.getMonths() > 0 + || period.getWeeks() > 0 + || period.getDays() > 0 + || period.getHours() > 0 + || period.getMinutes() > 0 + || period.getSeconds() > 0 + || period.getMillis() > 0; + } + + @JsonProperty + @Override + public String getId() + { + return id; + } + + @JsonProperty + @Override + public String getDescription() + { + return description; + } + + @JsonProperty + @Override + public Period getPeriod() + { + return period; + } + + @Override + public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTime) + { + DateTime now = DateTimes.nowUtc(); + DateTime intervalEnd = interval.getEnd(); + DateTime intervalStart = interval.getStart(); + + if (referenceTime != null) { + // Use the provided reference time instead of actual "now" for checking rule applicability + now = referenceTime; + } + DateTime threshold = now.minus(period); + + if (intervalEnd.isBefore(threshold) || intervalEnd.isEqual(threshold)) { + LOG.debug("Compaction rule [%s] applies FULLY to interval [%s]. Threshold: [%s]", id, interval, threshold); + return AppliesToMode.FULL; + } else if (intervalStart.isAfter(threshold)) { + LOG.debug("Compaction rule [%s] does NOT apply to interval [%s]. Threshold: [%s]", id, interval, threshold); + return AppliesToMode.NONE; + } else { + LOG.debug("Compaction rule [%s] applies PARTIALLY to interval [%s]. Threshold: [%s]", id, interval, threshold); + return AppliesToMode.PARTIAL; + } + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java new file mode 100644 index 000000000000..e3a891e91a7f --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java @@ -0,0 +1,86 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction dimensions rule that specifies dimension schema for segments older than a specified period. + *

+ * This rule defines which dimensions to include in compacted segments and their types. For example, + * dropping unused dimensions from older data can reduce storage size and improve query performance. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply + * to any segment where the segment's end time is before ("now" - 90 days). + *

+ * This is a non-additive rule. Multiple dimensions rules cannot be applied to the same interval safely, + * as a segment can only have one dimensions specification. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "optimize-dimensions-90d",
+ *   "period": "P90D",
+ *   "dimensionsSpec": {
+ *     "dimensions": [
+ *       "country",
+ *       "city",
+ *       { "type": "long", "name": "user_id" }
+ *     ]
+ *   },
+ *   "description": "Optimize dimension schema for data older than 90 days"
+ * }
+ * }
+ */ +public class CompactionDimensionsRule extends AbstractCompactionRule +{ + private final UserCompactionTaskDimensionsConfig dimensionsSpec; + + @JsonCreator + public CompactionDimensionsRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("dimensionsSpec") @Nonnull UserCompactionTaskDimensionsConfig dimensionsSpec + ) + { + super(id, description, period); + this.dimensionsSpec = Objects.requireNonNull(dimensionsSpec, "dimensionsSpec cannot be null"); + } + + @Override + public boolean isAdditive() + { + return false; + } + + @JsonProperty + public UserCompactionTaskDimensionsConfig getDimensionsSpec() + { + return dimensionsSpec; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java new file mode 100644 index 000000000000..64794619af9e --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java @@ -0,0 +1,87 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.query.filter.DimFilter; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction filter rule that specifies rows to remove from segments older than a specified period. + *

+ * The filter defines rows to REMOVE from compacted segments. For example, a filter + * {@code selector(isRobot=true)} means "remove rows where isRobot=true". The compaction framework + * automatically wraps these filters in NOT logic during processing. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply + * to any segment where the segment's end time is before ("now" - 90 days). + *

+ * Multiple rules can apply to the same segment. When multiple rules apply, they are combined as + * NOT(A OR B OR C) for optimal bitmap performance, which is equivalent to NOT A AND NOT B AND NOT C + * but uses fewer operations. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "remove-robots-90d",
+ *   "period": "P90D",
+ *   "filter": {
+ *     "type": "selector",
+ *     "dimension": "isRobot",
+ *     "value": "true"
+ *   },
+ *   "description": "Remove robot traffic from segments older than 90 days"
+ * }
+ * }
+ */ +public class CompactionFilterRule extends AbstractCompactionRule +{ + + private final DimFilter filter; + + @JsonCreator + public CompactionFilterRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("filter") @Nonnull DimFilter filter + ) + { + super(id, description, period); + this.filter = Objects.requireNonNull(filter, "filter cannot be null"); + } + + @Override + public boolean isAdditive() + { + return true; + } + + @JsonProperty + public DimFilter getFilter() + { + return filter; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java new file mode 100644 index 000000000000..5e7932ce0e06 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java @@ -0,0 +1,84 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction granularity rule that specifies segment and query granularity for segments older than a specified period. + *

+ * This rule controls how time-series data is bucketed during compaction. For example, changing from + * 15-minute segments to hourly segments reduces segment count and improves query performance for + * older data that doesn't require fine-grained time resolution. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P7D will apply + * to any segment where the segment's end time is before ("now" - 7 days). + *

+ * This is a non-additive rule. Multiple granularity rules cannot be applied to the same interval safely, + * as a segment can only have one granularity. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "daily-30d",
+ *   "period": "P30D",
+ *   "granularityConfig": {
+ *     "segmentGranularity": "DAY",
+ *     "queryGranularity": "HOUR"
+ *   },
+ *   "description": "Compact to daily segments for data older than 30 days"
+ * }
+ * }
+ */ +public class CompactionGranularityRule extends AbstractCompactionRule +{ + + private final UserCompactionTaskGranularityConfig granularityConfig; + + @JsonCreator + public CompactionGranularityRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("granularityConfig") @Nonnull UserCompactionTaskGranularityConfig granularityConfig) + { + super(id, description, period); + this.granularityConfig = Objects.requireNonNull(granularityConfig, "granularityConfig cannot be null"); + } + + @Override + public boolean isAdditive() + { + return false; + } + + @JsonProperty + public UserCompactionTaskGranularityConfig getGranularityConfig() + { + return granularityConfig; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java new file mode 100644 index 000000000000..3e2cb08a20eb --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java @@ -0,0 +1,78 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction IO config rule that specifies input/output configuration for segments older than a specified period. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P30D will apply + * to any segment where the segment's end time is before ("now" - 30 days). + *

+ * This is a non-additive rule. Multiple IO config rules cannot be applied to the same interval safely, + * as a compaction job can only use one IO configuration. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "dropExistingFalse-false",
+ *   "period": "P90D",
+ *   "ioConfig": {
+ *     "dropExisting": false
+ *   },
+ * }
+ * }
+ */ +public class CompactionIOConfigRule extends AbstractCompactionRule +{ + private final UserCompactionTaskIOConfig ioConfig; + + @JsonCreator + public CompactionIOConfigRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("ioConfig") @Nonnull UserCompactionTaskIOConfig ioConfig + ) + { + super(id, description, period); + this.ioConfig = Objects.requireNonNull(ioConfig, "ioConfig cannot be null"); + } + + @Override + public boolean isAdditive() + { + return false; + } + + @JsonProperty + public UserCompactionTaskIOConfig getIoConfig() + { + return ioConfig; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java new file mode 100644 index 000000000000..72d4ae0602f1 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java @@ -0,0 +1,85 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.query.aggregation.AggregatorFactory; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction metrics rule that specifies aggregation metrics for segments older than a specified period. + *

+ * This rule defines the metrics specification used during compaction, enabling rollup and pre-aggregation + * of older data. For example, applying sum and count aggregators to historical data can significantly + * reduce storage size while preserving queryability for common aggregation queries. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply + * to any segment where the segment's end time is before ("now" - 90 days). + *

+ * This is a non-additive rule. Multiple metrics rules cannot be applied to the same interval safely, + * as a segment can only have one metrics specification. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "rollup-90d",
+ *   "period": "P90D",
+ *   "metricsSpec": [
+ *     { "type": "count", "name": "count" },
+ *     { "type": "longSum", "name": "total_views", "fieldName": "views" }
+ *   ],
+ *   "description": "Enable rollup for data older than 90 days"
+ * }
+ * }
+ */ +public class CompactionMetricsRule extends AbstractCompactionRule +{ + private final AggregatorFactory[] metricsSpec; + + @JsonCreator + public CompactionMetricsRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("metricsSpec") @Nonnull AggregatorFactory[] metricsSpec + ) + { + super(id, description, period); + this.metricsSpec = Objects.requireNonNull(metricsSpec, "metricsSpec cannot be null"); + } + + @Override + public boolean isAdditive() + { + return false; + } + + @JsonProperty + @Nonnull + public AggregatorFactory[] getMetricsSpec() + { + return metricsSpec; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java new file mode 100644 index 000000000000..6f784a9cfa7b --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java @@ -0,0 +1,89 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +/** + * A compaction projection rule that specifies aggregate projections to add to segments older than a specified period. + *

+ * This rule defines pre-aggregated views of data that can accelerate specific query patterns. Projections are + * particularly useful for older data where query patterns are well-understood and storage efficiency is valuable. + *

+ * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply + * to any segment where the segment's end time is before ("now" - 90 days). + *

+ * This is an additive rule. Multiple projection rules can apply to the same interval, and all projections + * are combined into a single list on the compacted segment. + *

+ * Example usage: + *

{@code
+ * {
+ *   "id": "hourly-projection-90d",
+ *   "period": "P90D",
+ *   "projections": [
+ *     {
+ *       "name": "hourly_agg",
+ *       "dimensions": ["country"],
+ *       "metrics": [
+ *         { "type": "longSum", "name": "total_views", "fieldName": "views" }
+ *       ]
+ *     }
+ *   ],
+ *   "description": "Add hourly aggregation projection for data older than 90 days"
+ * }
+ * }
+ */ +public class CompactionProjectionRule extends AbstractCompactionRule +{ + private final List projections; + + @JsonCreator + public CompactionProjectionRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("projections") @Nonnull List projections + ) + { + super(id, description, period); + this.projections = Objects.requireNonNull(projections, "projections cannot be null"); + } + + @Override + public boolean isAdditive() + { + return true; + } + + @JsonProperty + public List getProjections() + { + return projections; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java new file mode 100644 index 000000000000..bb6887e4738f --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java @@ -0,0 +1,84 @@ +/* + * 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.compaction; + +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import javax.annotation.Nullable; + +/** + * Defines a compaction configuration that applies to data based on age thresholds. + *

+ * Rules encapsulate specific aspects of compaction (granularity, filters, tuning, etc.) + * and specify when they should apply via a period threshold. + */ +public interface CompactionRule +{ + /** + * Indicates how a rule applies to a given time interval based on the rule's period threshold. + *

    + *
  • PARTIAL: The rule applies to part of the interval.
  • + *
  • FULL: The rule applies to the entire interval.
  • + *
  • NONE: The rule does not apply to the interval at all.
  • + */ + enum AppliesToMode + { + PARTIAL, + FULL, + NONE + } + + String getId(); + + String getDescription(); + + Period getPeriod(); + + /** + * Check if this rule applies to the given interval. + *

    + *

      + *
    • If the rule applies to the entire interval, return {@link AppliesToMode#FULL}.
    • + *
    • If the rule applies to only part of the interval, return {@link AppliesToMode#PARTIAL}.
    • + *
    • If the rule does not apply to the interval at all, return {@link AppliesToMode#NONE}.
    • + *
    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations. null results in using current UTC instant at runtime. + * @return The applicability mode of the rule for the given interval and reference time. + */ + AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTime); + + /** + * Indicates whether the rule is additive, meaning it can be combined with other rules. + *

    + * An additive rule can be merged with other rules of its type within the same interval. An example would be dimension + * filter rules that can be combined using OR logic. Such as, rule 1: filter out segments where country = 'US' OR + * rule 2: device = 'mobile'. + *

    + *

    + * A non-addditive rule cannot be combined with other rules of its type within the same interval. An example would be + * segment granularity rules, where only one granularity can be applied to a given interval. + *

    + */ + boolean isAdditive(); +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java new file mode 100644 index 000000000000..1d8f281b8743 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java @@ -0,0 +1,200 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import java.util.List; + +/** + * Provides compaction rules for different aspects of compaction configuration. + *

    + * This abstraction allows rules to be sourced from different locations: inline definitions, + * database storage, external services, or dynamically generated based on metrics. Each method + * returns rules for a specific compaction aspect (granularity, filters, tuning, etc.), either + * for all rules or filtered by interval applicability. + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonSubTypes(value = { + @JsonSubTypes.Type(name = InlineCompactionRuleProvider.TYPE, value = InlineCompactionRuleProvider.class), + @JsonSubTypes.Type(name = ComposingCompactionRuleProvider.TYPE, value = ComposingCompactionRuleProvider.class) +}) +public interface CompactionRuleProvider +{ + /** + * Returns the type identifier for this provider implementation. + *

    + * This value is used in JSON serialization to identify which provider implementation + * to use when deserializing. + * + * @return the type identifier (e.g., "inline", "external") + */ + String getType(); + + /** + * Returns true if this provider is ready to supply rules. + *

    + * Providers that depend on external state (HTTP services, databases) should return false + * until they have successfully initialized and loaded their rules. Compaction supervisors + * should check this before generating tasks to avoid creating tasks with incomplete rule sets. + *

    + * The default implementation returns true, which is appropriate for providers that have + * their rules available immediately (such as inline providers with static configuration). + * + * @return true if the provider is ready to supply rules, false otherwise + */ + default boolean isReady() + { + return true; + } + + /** + * Returns all unique periods used by the rules provided by this provider, condensed and sorted in ascending order. + *

    + * Ascending order means from shortest to longest period. For example, [P1D, P7D, P30D]. + *

    + */ + List getCondensedAndSortedPeriods(DateTime referenceTime); + + /** + * Returns all compaction filter rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionFilterRule} rules that apply to the given interval. + */ + List getFilterRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction filter rules. + */ + List getFilterRules(); + + /** + * Returns all compaction metrics rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionMetricsRule} rules that apply to the given interval. + */ + List getMetricsRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction metrics rules. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + */ + List getMetricsRules(); + + /** + * Returns all compaction dimensions rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionDimensionsRule} rules that apply to the given interval. + */ + List getDimensionsRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction dimensions rules. + */ + List getDimensionsRules(); + + /** + * Returns all compaction IO config rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionIOConfigRule} rules that apply to the given interval. + */ + List getIOConfigRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction IO config rules. + */ + List getIOConfigRules(); + + /** + * Returns all compaction projection rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionProjectionRule} rules that apply to the given interval. + */ + List getProjectionRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction projection rules. + */ + List getProjectionRules(); + + /** + * Returns all compaction granularity rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return The list of {@link CompactionGranularityRule} rules that apply to the given interval. + */ + List getGranularityRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction granularity rules. + */ + List getGranularityRules(); + + /** + * Returns all compaction tuning config rules that apply to the given interval. + *

    + * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + *

    + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + */ + List getTuningConfigRules(Interval interval, DateTime referenceTime); + + /** + * Returns ALL compaction tuning config rules. + */ + List getTuningConfigRules(); +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 99e1eef21465..6e79488c70b7 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -32,6 +32,9 @@ import org.apache.druid.java.util.common.granularity.GranularityType; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.transform.CompactionTransformSpec; @@ -45,9 +48,12 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -60,6 +66,7 @@ public class CompactionStatus private static final Logger log = new Logger(CompactionStatus.class); private static final CompactionStatus COMPLETE = new CompactionStatus(State.COMPLETE, null, null, null); + public static final String NEVER_COMPACTED_REASON = "not compacted yet"; public enum State { @@ -251,6 +258,113 @@ public static CompactionStatus running(String message) return new CompactionStatus(State.RUNNING, message, null, null); } + /** + * Computes the required set of filter rules to be applied for the given compaction candidate. + *

    + * We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces + * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. + *

    + * + * @param candidateSegments + * @param expectedFilter + * @param compactionStateCache + * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied + */ + @Nullable + public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( + CompactionCandidate candidateSegments, + NotDimFilter expectedFilter, + CompactionStateCache compactionStateCache + ) + { + if (!(expectedFilter.getField() instanceof OrDimFilter)) { + return expectedFilter; + } + + List expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); + + // Collect unique fingerprints + Set uniqueFingerprints = candidateSegments.getSegments().stream() + .map(DataSegment::getCompactionStateFingerprint) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (uniqueFingerprints.isEmpty()) { + // no fingerprints means that no candidate segments have transforms to compare against. Return all filters eagerly. + return expectedFilter; + } + + // Accumulate filters that haven't been applied across all fingerprints + Set unappliedRules = new HashSet<>(); + + for (String fingerprint : uniqueFingerprints) { + CompactionState state = compactionStateCache.getCompactionStateByFingerprint(fingerprint).orElse(null); + + if (state == null) { + // Safety: if state is missing, return all filters eagerly since we can't determine applied filters + return expectedFilter; + } + + // Extract applied filters from the CompactionState into a Set + Set appliedFilters = extractAppliedFilters(state); + + // If transform spec or filter for the CompactionState is null, return all expected filters eagerly + if (appliedFilters == null) { + return expectedFilter; + } + + // Check which expected filters are NOT in the applied set and add them to unappliedRules + for (DimFilter expected : expectedFilters) { + if (!appliedFilters.contains(expected)) { + unappliedRules.add(expected); + } + } + } + + log.info( + "Computed [%d] unapplied rules out of [%d] possible rules for candidate", + unappliedRules.size(), + expectedFilters.size() + ); + + // If all filters were applied, return null + if (unappliedRules.isEmpty()) { + return null; + } + + // Return the delta as NOT(OR(unapplied filters)) + return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); + } + + /** + * Extracts applied filters from a CompactionState. + * Returns null if transform spec or filter is null (indicating all filters should be applied). + */ + @Nullable + private static Set extractAppliedFilters(CompactionState state) + { + if (state.getTransformSpec() == null) { + return null; + } + + DimFilter filter = state.getTransformSpec().getFilter(); + if (filter == null) { + return null; + } + + if (!(filter instanceof NotDimFilter)) { + return Collections.emptySet(); + } + + DimFilter inner = ((NotDimFilter) filter).getField(); + + if (inner instanceof OrDimFilter) { + return new HashSet<>(((OrDimFilter) inner).getFields()); + } else { + return Collections.singleton(inner); + } + } + /** * Determines the CompactionStatus of the given candidate segments by evaluating * the {@link #CHECKS} one by one. If any check returns an incomplete status, @@ -522,7 +636,7 @@ private CompactionStatus segmentsHaveBeenCompactedAtLeastOnce() if (uncompactedSegments.isEmpty()) { return COMPLETE; } else { - return CompactionStatus.pending("not compacted yet"); + return CompactionStatus.pending(NEVER_COMPACTED_REASON); } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java new file mode 100644 index 000000000000..bc1cfc4c242c --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java @@ -0,0 +1,87 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A compaction tuning config rule that specifies tuning parameters for segments older than a specified period. + *

    + * This rule controls partitioning strategy, indexing behavior, and resource usage during compaction. + * For example, applying range partitioning with specific dimensions to older data can optimize + * query performance for common access patterns. + *

    + * Rules are evaluated at compaction time based on segment age. A rule with period P30D will apply + * to any segment where the segment's end time is before ("now" - 30 days). + *

    + * This is a non-additive rule. Multiple tuning config rules cannot be applied to the same interval safely, + * as a compaction job can only use one tuning configuration. + *

    + * Example usage: + *

    {@code
    + * {
    + *   "id": "range-partition-30d",
    + *   "period": "P30D",
    + *   "tuningConfig": {
    + *     "partitionsSpec": {
    + *       "type": "range",
    + *       "targetRowsPerSegment": 5000000,
    + *       "partitionDimensions": ["country", "city"]
    + *     }
    + *   },
    + *   "description": "Use range partitioning for data older than 30 days"
    + * }
    + * }
    + */ +public class CompactionTuningConfigRule extends AbstractCompactionRule +{ + private final UserCompactionTaskQueryTuningConfig tuningConfig; + + @JsonCreator + public CompactionTuningConfigRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("period") @Nonnull Period period, + @JsonProperty("tuningConfig") @Nonnull UserCompactionTaskQueryTuningConfig tuningConfig + ) + { + super(id, description, period); + this.tuningConfig = Objects.requireNonNull(tuningConfig, "tuningConfig cannot be null"); + } + + @Override + public boolean isAdditive() + { + return false; + } + + @JsonProperty + public UserCompactionTaskQueryTuningConfig getTuningConfig() + { + return tuningConfig; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java new file mode 100644 index 000000000000..3bb35bc3a26c --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java @@ -0,0 +1,306 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * A meta-provider that composes multiple {@link CompactionRuleProvider}s with first-wins semantics. + *

    + * This provider delegates rule queries to a list of child providers in order. For each rule type, + * it returns the result from the first provider that has non-empty rules of that type. + *

    + * First-Wins Strategy: + * Provider order determines precedence. If provider A returns rules of a given type, those rules + * are used and subsequent providers are not consulted for that type. This applies to all rule + * types, regardless of whether they are additive or non-additive. + *

    + * Readiness: + * The composing provider is considered ready only when ALL child providers are ready. + * This ensures consistent behavior during startup. + *

    + * Example Usage: + *

    {@code
    + * {
    + *   "type": "composing",
    + *   "providers": [
    + *     {
    + *       "type": "inline",
    + *       "granularityRules": [{
    + *         "id": "recent-data-granularity",
    + *         "period": "P7D",
    + *         "granularity": "HOUR"
    + *       }]
    + *     },
    + *     {
    + *       "type": "inline",
    + *       "granularityRules": [{
    + *         "id": "default-granularity",
    + *         "period": "P1D",
    + *         "granularity": "DAY"
    + *       }],
    + *       "filterRules": [{
    + *         "id": "remove-bots",
    + *         "period": "P30D",
    + *         "filter": {
    + *           "type": "selector",
    + *           "dimension": "isRobot",
    + *           "value": "true"
    + *         }
    + *       }]
    + *     }
    + *   ]
    + * }
    + * }
    + * In this example: + *
      + *
    • Granularity rules come from the first provider (HOUR granularity for recent data)
    • + *
    • Filter rules come from the second provider (first provider with filters)
    • + *
    + */ +public class ComposingCompactionRuleProvider implements CompactionRuleProvider +{ + public static final String TYPE = "composing"; + + private final List providers; + + @JsonCreator + public ComposingCompactionRuleProvider( + @JsonProperty("providers") List providers + ) + { + this.providers = Objects.requireNonNull(providers, "providers cannot be null"); + + // Validate that no provider in the list is null + for (int i = 0; i < providers.size(); i++) { + if (providers.get(i) == null) { + throw new NullPointerException("provider at index " + i + " is null"); + } + } + } + + @JsonProperty("providers") + public List getProviders() + { + return providers; + } + + @Override + public String getType() + { + return TYPE; + } + + @Override + public boolean isReady() + { + // All providers must be ready + return providers.stream().allMatch(CompactionRuleProvider::isReady); + } + + @Override + public List getCondensedAndSortedPeriods(DateTime referenceTime) + { + // Collect all unique periods from all providers, sorted ascending + return providers.stream() + .flatMap(p -> p.getCondensedAndSortedPeriods(referenceTime).stream()) + .distinct() + .sorted(Comparator.comparing(Period::toStandardDuration)) + .collect(Collectors.toList()); + } + + @Override + public List getFilterRules() + { + return providers.stream() + .map(CompactionRuleProvider::getFilterRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getFilterRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getFilterRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getMetricsRules() + { + return providers.stream() + .map(CompactionRuleProvider::getMetricsRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getMetricsRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getMetricsRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getDimensionsRules() + { + return providers.stream() + .map(CompactionRuleProvider::getDimensionsRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getDimensionsRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getDimensionsRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getIOConfigRules() + { + return providers.stream() + .map(CompactionRuleProvider::getIOConfigRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getIOConfigRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getIOConfigRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getProjectionRules() + { + return providers.stream() + .map(CompactionRuleProvider::getProjectionRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getProjectionRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getProjectionRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getGranularityRules() + { + return providers.stream() + .map(CompactionRuleProvider::getGranularityRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getGranularityRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getGranularityRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getTuningConfigRules() + { + return providers.stream() + .map(CompactionRuleProvider::getTuningConfigRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public List getTuningConfigRules(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getTuningConfigRules(interval, referenceTime)) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ComposingCompactionRuleProvider that = (ComposingCompactionRuleProvider) o; + return Objects.equals(providers, that.providers); + } + + @Override + public int hashCode() + { + return Objects.hash(providers); + } + + @Override + public String toString() + { + return "ComposingCompactionRuleProvider{" + + "providers=" + providers + + ", ready=" + isReady() + + '}'; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java new file mode 100644 index 000000000000..c58af2f694af --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java @@ -0,0 +1,390 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.common.config.Configs; +import org.joda.time.DateTime; +import org.joda.time.Duration; +import org.joda.time.Interval; +import org.joda.time.Period; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Rule provider that returns a static list of rules defined inline in the configuration. + *

    + * This is the simplest provider implementation, suitable for testing and use cases where the number of rules is + * relatively small and can be defined directly in the compaction config. + *

    + * When filtering rules by interval, this provider only returns rules where {@link CompactionRule#appliesTo(Interval, DateTime)} + * returns {@link CompactionRule.AppliesToMode#FULL}. Rules with partial or no overlap are excluded. + *

    + * For non-additive rule types, when multiple rules fully match an interval, only the rule with the oldest threshold + * (largest period) is returned. For example, if both a P30D and P90D granularity rule match an interval, the P90D + * rule is selected because it has the oldest threshold (now - 90 days is older than now - 30 days). + *

    + * Example usage: + *

    {@code
    + * {
    + *   "type": "inline",
    + *   "compactionFilterRules": [
    + *     {
    + *       "id": "remove-bots-90d",
    + *       "period": "P90D",
    + *       "filter": {
    + *         "type": "not",
    + *         "field": {
    + *           "type": "selector",
    + *           "dimension": "is_bot",
    + *           "value": "true"
    + *         }
    + *       },
    + *       "description": "Remove bot traffic from segments older than 90 days"
    + *     },
    + *     {
    + *       "id": "remove-low-priority-180d",
    + *       "period": "P180D",
    + *       "filter": {
    + *         "type": "not",
    + *         "field": {
    + *           "type": "in",
    + *           "dimension": "priority",
    + *           "values": ["low", "spam"]
    + *         }
    + *       },
    + *       "description": "Remove low-priority data from segments older than 180 days"
    + *     }
    + *   ]
    + * }
    + * }
    + */ +public class InlineCompactionRuleProvider implements CompactionRuleProvider +{ + public static final String TYPE = "inline"; + + private final List compactionFilterRules; + private final List compactionMetricsRules; + private final List compactionDimensionsRules; + private final List compactionIOConfigRules; + private final List compactionProjectionRules; + private final List compactionGranularityRules; + private final List compactionTuningConfigRules; + + + @JsonCreator + public InlineCompactionRuleProvider( + @JsonProperty("compactionFilterRules") @Nullable List compactionFilterRules, + @JsonProperty("compactionMetricsRules") @Nullable List compactionMetricsRules, + @JsonProperty("compactionDimensionsRules") @Nullable List compactionDimensionsRules, + @JsonProperty("compactionIOConfigRules") @Nullable List compactionIOConfigRules, + @JsonProperty("compactionProjectionRules") @Nullable List compactionProjectionRules, + @JsonProperty("compactionGranularityRules") @Nullable List compactionGranularityRules, + @JsonProperty("compactionTuningConfigRules") @Nullable List compactionTuningConfigRules + ) + { + this.compactionFilterRules = Configs.valueOrDefault(compactionFilterRules, Collections.emptyList()); + this.compactionMetricsRules = Configs.valueOrDefault(compactionMetricsRules, Collections.emptyList()); + this.compactionDimensionsRules = Configs.valueOrDefault(compactionDimensionsRules, Collections.emptyList()); + this.compactionIOConfigRules = Configs.valueOrDefault(compactionIOConfigRules, Collections.emptyList()); + this.compactionProjectionRules = Configs.valueOrDefault(compactionProjectionRules, Collections.emptyList()); + this.compactionGranularityRules = Configs.valueOrDefault(compactionGranularityRules, Collections.emptyList()); + this.compactionTuningConfigRules = Configs.valueOrDefault(compactionTuningConfigRules, Collections.emptyList()); + } + + public static Builder builder() + { + return new Builder(); + } + + @Override + @JsonProperty("type") + public String getType() + { + return TYPE; + } + + @Override + @JsonProperty("compactionFilterRules") + public List getFilterRules() + { + return compactionFilterRules; + } + + @Override + @JsonProperty("compactionMetricsRules") + public List getMetricsRules() + { + return compactionMetricsRules; + } + + @Override + @JsonProperty("compactionDimensionsRules") + public List getDimensionsRules() + { + return compactionDimensionsRules; + } + + @Override + @JsonProperty("compactionIOConfigRules") + public List getIOConfigRules() + { + return compactionIOConfigRules; + } + + @Override + @JsonProperty("compactionProjectionRules") + public List getProjectionRules() + { + return compactionProjectionRules; + } + + @Override + @JsonProperty("compactionGranularityRules") + public List getGranularityRules() + { + return compactionGranularityRules; + } + + @Override + @JsonProperty("compactionTuningConfigRules") + public List getTuningConfigRules() + { + return compactionTuningConfigRules; + } + + @Override + public List getCondensedAndSortedPeriods(DateTime referenceTime) + { + return Stream.of( + compactionFilterRules, + compactionMetricsRules, + compactionDimensionsRules, + compactionIOConfigRules, + compactionProjectionRules, + compactionGranularityRules, + compactionTuningConfigRules + ) + .flatMap(List::stream) + .map(CompactionRule::getPeriod) + .distinct() + .sorted(Comparator.comparingLong(period -> { + DateTime endTime = referenceTime.plus(period); + return new Duration(referenceTime, endTime).getMillis(); + })) + .collect(Collectors.toList()); + + } + + @Override + public List getFilterRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionFilterRules, interval, referenceTime); + } + + @Override + public List getMetricsRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionMetricsRules, interval, referenceTime); + } + + @Override + public List getDimensionsRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionDimensionsRules, interval, referenceTime); + } + + @Override + public List getIOConfigRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionIOConfigRules, interval, referenceTime); + } + + @Override + public List getProjectionRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionProjectionRules, interval, referenceTime); + } + + @Override + public List getGranularityRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionGranularityRules, interval, referenceTime); + } + + @Override + public List getTuningConfigRules(Interval interval, DateTime referenceTime) + { + return getApplicableRules(compactionTuningConfigRules, interval, referenceTime); + } + + /** + * Returns the list of rules that apply to the given interval. + *

    + * This provider implementation only returns rules that fully apply to the given interval. + *

    + * Any non-additive rule types will only return a single rule, even if multiple rules fully apply to the interval. The + * interval returned is the one with the oldest threshold (i.e., the largest period into the past from "now"). + */ + private List getApplicableRules(List rules, Interval interval, DateTime referenceTime) + { + boolean areRulesAdditive = false; + List applicableRules = new ArrayList<>(); + for (T rule : rules) { + areRulesAdditive = rule.isAdditive(); + if (rule.appliesTo(interval, referenceTime) == CompactionRule.AppliesToMode.FULL) { + applicableRules.add(rule); + } + } + if (!areRulesAdditive && applicableRules.size() > 1) { + // if rules are not additive, I want the period where (referenceTime - period) is the oldest date of all the rules + T selectedRule = Collections.min( + applicableRules, + Comparator.comparingLong(r -> { + DateTime threshold = referenceTime.minus(r.getPeriod()); + return threshold.getMillis(); + }) + ); + applicableRules = List.of(selectedRule); + } + return applicableRules; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + InlineCompactionRuleProvider that = (InlineCompactionRuleProvider) o; + return Objects.equals(compactionFilterRules, that.compactionFilterRules) + && Objects.equals(compactionMetricsRules, that.compactionMetricsRules) + && Objects.equals(compactionDimensionsRules, that.compactionDimensionsRules) + && Objects.equals(compactionIOConfigRules, that.compactionIOConfigRules) + && Objects.equals(compactionProjectionRules, that.compactionProjectionRules) + && Objects.equals(compactionGranularityRules, that.compactionGranularityRules) + && Objects.equals(compactionTuningConfigRules, that.compactionTuningConfigRules); + } + + @Override + public int hashCode() + { + return Objects.hash( + compactionFilterRules, + compactionMetricsRules, + compactionDimensionsRules, + compactionIOConfigRules, + compactionProjectionRules, + compactionGranularityRules, + compactionTuningConfigRules + ); + } + + @Override + public String toString() + { + return "InlineCompactionRuleProvider{" + + "compactionFilterRules=" + compactionFilterRules + + ", compactionMetricsRules=" + compactionMetricsRules + + ", compactionDimensionsRules=" + compactionDimensionsRules + + ", compactionIOConfigRules=" + compactionIOConfigRules + + ", compactionProjectionRules=" + compactionProjectionRules + + ", compactionGranularityRules=" + compactionGranularityRules + + ", compactionTuningConfigRules=" + compactionTuningConfigRules + + '}'; + } + + public static class Builder + { + private List compactionFilterRules; + private List compactionMetricsRules; + private List compactionDimensionsRules; + private List compactionIOConfigRules; + private List compactionProjectionRules; + private List compactionGranularityRules; + private List compactionTuningConfigRules; + + public Builder filterRules(List compactionFilterRules) + { + this.compactionFilterRules = compactionFilterRules; + return this; + } + + public Builder metricsRules(List compactionMetricsRules) + { + this.compactionMetricsRules = compactionMetricsRules; + return this; + } + + public Builder dimensionsRules(List compactionDimensionsRules) + { + this.compactionDimensionsRules = compactionDimensionsRules; + return this; + } + + public Builder ioConfigRules(List compactionIOConfigRules) + { + this.compactionIOConfigRules = compactionIOConfigRules; + return this; + } + + public Builder projectionRules(List compactionProjectionRules) + { + this.compactionProjectionRules = compactionProjectionRules; + return this; + } + + public Builder granularityRules(List compactionGranularityRules) + { + this.compactionGranularityRules = compactionGranularityRules; + return this; + } + + public Builder tuningConfigRules(List compactionTuningConfigRules) + { + this.compactionTuningConfigRules = compactionTuningConfigRules; + return this; + } + + public InlineCompactionRuleProvider build() + { + return new InlineCompactionRuleProvider( + compactionFilterRules, + compactionMetricsRules, + compactionDimensionsRules, + compactionIOConfigRules, + compactionProjectionRules, + compactionGranularityRules, + compactionTuningConfigRules + ); + } + } +} diff --git a/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java b/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java index b88c4a0bcaf4..d75d22dda5c5 100644 --- a/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java +++ b/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java @@ -276,6 +276,29 @@ public int hashCode() return result; } + /** + * Creates a builder initialized with all fields from this config. + * Useful for creating modified copies of an existing config. + */ + public Builder toBuilder() + { + return new Builder() + .forDataSource(this.dataSource) + .withTaskPriority(this.taskPriority) + .withInputSegmentSizeBytes(this.inputSegmentSizeBytes) + .withMaxRowsPerSegment(this.maxRowsPerSegment) + .withSkipOffsetFromLatest(this.skipOffsetFromLatest) + .withTuningConfig(this.tuningConfig) + .withGranularitySpec(this.granularitySpec) + .withDimensionsSpec(this.dimensionsSpec) + .withMetricsSpec(this.metricsSpec) + .withTransformSpec(this.transformSpec) + .withProjections(this.projections) + .withIoConfig(this.ioConfig) + .withEngine(this.engine) + .withTaskContext(this.taskContext); + } + public static class Builder { private String dataSource; diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java new file mode 100644 index 000000000000..6126b6adf25f --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java @@ -0,0 +1,172 @@ +/* + * 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.compaction; + +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionDimensionsRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_14_DAYS = Period.days(14); + + private final CompactionDimensionsRule rule = new CompactionDimensionsRule( + "test-dimensions-rule", + "Custom dimensions config", + PERIOD_14_DAYS, + new UserCompactionTaskDimensionsConfig(null) + ); + + @Test + public void test_isAdditive_returnsFalse() + { + // Dimensions rules are not additive - only one dimensions spec can apply + Assert.assertFalse(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval ends at 2025-12-03, which is fully before threshold + Interval interval = new Interval("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getDimensionsSpec_returnsConfiguredValue() + { + UserCompactionTaskDimensionsConfig spec = rule.getDimensionsSpec(); + + Assert.assertNotNull(spec); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-dimensions-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Custom dimensions config", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_14_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionDimensionsRule(null, "description", PERIOD_14_DAYS, config) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionDimensionsRule("test-id", "description", null, config) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionDimensionsRule("test-id", "description", zeroPeriod, config) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); + Period negativePeriod = Period.days(-14); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionDimensionsRule("test-id", "description", negativePeriod, config) + ); + } + + @Test + public void test_constructor_nullDimensionsSpec_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionDimensionsRule("test-id", "description", PERIOD_14_DAYS, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java new file mode 100644 index 000000000000..6a7e1bc571fa --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java @@ -0,0 +1,331 @@ +/* + * 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.compaction; + +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionFilterRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_30_DAYS = Period.days(30); + + private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); + private final CompactionFilterRule rule = new CompactionFilterRule( + "test-filter-rule", + "Remove robot traffic", + PERIOD_30_DAYS, + testFilter + ); + + @Test + public void test_isAdditive_returnsTrue() + { + // Filter rules are additive - multiple filters can be combined + Assert.assertTrue(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) + // Interval ends at 2025-11-15, which is fully before threshold + Interval interval = new Interval("2025-11-14T00:00:00Z/2025-11-15T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-11-18T12:00:00Z/2025-11-19T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-11-18T00:00:00Z/2025-11-20T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getFilter_returnsConfiguredFilter() + { + DimFilter filter = rule.getFilter(); + + Assert.assertNotNull(filter); + Assert.assertEquals(testFilter, filter); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-filter-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Remove robot traffic", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_30_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionFilterRule(null, "description", PERIOD_30_DAYS, testFilter) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionFilterRule("test-id", "description", null, testFilter) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionFilterRule("test-id", "description", zeroPeriod, testFilter) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-30); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionFilterRule("test-id", "description", negativePeriod, testFilter) + ); + } + + @Test + public void test_constructor_nullFilter_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionFilterRule("test-id", "description", PERIOD_30_DAYS, null) + ); + } + + // ========== Tests for variable-length periods (months/years) ========== + + @Test + public void test_constructor_periodWithMonths_succeeds() + { + // P6M should work - months are valid even though they're variable length + Period period = Period.months(6); + CompactionFilterRule rule = new CompactionFilterRule( + "test-id", + "6 month rule", + period, + testFilter + ); + + Assert.assertEquals(period, rule.getPeriod()); + } + + @Test + public void test_constructor_periodWithYears_succeeds() + { + // P1Y should work - years are valid even though they're variable length + Period period = Period.years(1); + CompactionFilterRule rule = new CompactionFilterRule( + "test-id", + "1 year rule", + period, + testFilter + ); + + Assert.assertEquals(period, rule.getPeriod()); + } + + @Test + public void test_constructor_periodWithMixedMonthsAndDays_succeeds() + { + // P6M15D should work - mixed months and days + Period period = Period.months(6).plusDays(15); + CompactionFilterRule rule = new CompactionFilterRule( + "test-id", + "6 months 15 days rule", + period, + testFilter + ); + + Assert.assertEquals(period, rule.getPeriod()); + } + + @Test + public void test_constructor_periodWithYearsMonthsDays_succeeds() + { + // P1Y3M10D should work - complex period with years, months, and days + Period period = Period.years(1).plusMonths(3).plusDays(10); + CompactionFilterRule rule = new CompactionFilterRule( + "test-id", + "1 year 3 months 10 days rule", + period, + testFilter + ); + + Assert.assertEquals(period, rule.getPeriod()); + } + + @Test + public void test_constructor_zeroMonthsPeriod_throwsIllegalArgumentException() + { + // P0M should fail - all components are zero/non-positive + Period zeroPeriod = Period.months(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionFilterRule("test-id", "description", zeroPeriod, testFilter) + ); + } + + @Test + public void test_constructor_negativeMonthsPeriod_throwsIllegalArgumentException() + { + // P-6M should fail - negative months + Period negativePeriod = Period.months(-6); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionFilterRule("test-id", "description", negativePeriod, testFilter) + ); + } + + @Test + public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() + { + // Test that month-based periods correctly calculate threshold using calendar arithmetic + // Reference time: 2025-12-19T12:00:00Z + // Period: P6M (6 months) + // Expected threshold: 2025-06-19T12:00:00Z (6 months before reference) + + Period sixMonths = Period.months(6); + CompactionFilterRule monthRule = new CompactionFilterRule( + "test-month-rule", + "6 months rule", + sixMonths, + testFilter + ); + + // Interval ending before 6-month threshold - should be FULL + Interval beforeThreshold = new Interval("2025-06-01T00:00:00Z/2025-06-15T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.FULL, + monthRule.appliesTo(beforeThreshold, REFERENCE_TIME) + ); + + // Interval spanning the 6-month threshold - should be PARTIAL + Interval spanningThreshold = new Interval("2025-06-15T00:00:00Z/2025-07-15T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.PARTIAL, + monthRule.appliesTo(spanningThreshold, REFERENCE_TIME) + ); + + // Interval starting after 6-month threshold - should be NONE + Interval afterThreshold = new Interval("2025-07-01T00:00:00Z/2025-07-15T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.NONE, + monthRule.appliesTo(afterThreshold, REFERENCE_TIME) + ); + } + + @Test + public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() + { + // Test that year-based periods correctly calculate threshold using calendar arithmetic + // Reference time: 2025-12-19T12:00:00Z + // Period: P1Y (1 year) + // Expected threshold: 2024-12-19T12:00:00Z (1 year before reference) + + Period oneYear = Period.years(1); + CompactionFilterRule yearRule = new CompactionFilterRule( + "test-year-rule", + "1 year rule", + oneYear, + testFilter + ); + + // Interval ending before 1-year threshold - should be FULL + Interval beforeThreshold = new Interval("2024-11-01T00:00:00Z/2024-12-01T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.FULL, + yearRule.appliesTo(beforeThreshold, REFERENCE_TIME) + ); + + // Interval spanning the 1-year threshold - should be PARTIAL + Interval spanningThreshold = new Interval("2024-12-01T00:00:00Z/2025-01-01T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.PARTIAL, + yearRule.appliesTo(spanningThreshold, REFERENCE_TIME) + ); + + // Interval starting after 1-year threshold - should be NONE + Interval afterThreshold = new Interval("2025-01-01T00:00:00Z/2025-02-01T00:00:00Z"); + Assert.assertEquals( + CompactionRule.AppliesToMode.NONE, + yearRule.appliesTo(afterThreshold, REFERENCE_TIME) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java new file mode 100644 index 000000000000..f11b5dc3777f --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java @@ -0,0 +1,190 @@ +/* + * 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.compaction; + +import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionGranularityRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_7_DAYS = Period.days(7); + + private final CompactionGranularityRule rule = new CompactionGranularityRule( + "test-rule", + "Test granularity rule", + PERIOD_7_DAYS, + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + + @Test + public void test_isAdditive_returnsFalse() + { + // Granularity rules are not additive - only one granularity can apply + Assert.assertFalse(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval ends at 2025-12-10, which is fully before threshold + Interval interval = new Interval("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getGranularityConfig_returnsConfiguredValue() + { + UserCompactionTaskGranularityConfig config = rule.getGranularityConfig(); + + Assert.assertNotNull(config); + Assert.assertEquals(Granularities.HOUR, config.getSegmentGranularity()); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Test granularity rule", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_7_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( + Granularities.HOUR, + null, + null + ); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionGranularityRule(null, "description", PERIOD_7_DAYS, config) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( + Granularities.HOUR, + null, + null + ); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionGranularityRule("test-id", "description", null, config) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( + Granularities.HOUR, + null, + null + ); + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionGranularityRule("test-id", "description", zeroPeriod, config) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( + Granularities.HOUR, + null, + null + ); + Period negativePeriod = Period.days(-7); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionGranularityRule("test-id", "description", negativePeriod, config) + ); + } + + @Test + public void test_constructor_nullGranularityConfig_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionGranularityRule("test-id", "description", PERIOD_7_DAYS, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java new file mode 100644 index 000000000000..5414eae9f1bd --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java @@ -0,0 +1,172 @@ +/* + * 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.compaction; + +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionIOConfigRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_60_DAYS = Period.days(60); + + private final CompactionIOConfigRule rule = new CompactionIOConfigRule( + "test-ioconfig-rule", + "Custom IO config", + PERIOD_60_DAYS, + new UserCompactionTaskIOConfig(null) + ); + + @Test + public void test_isAdditive_returnsFalse() + { + // IO config rules are not additive - only one IO config can apply + Assert.assertFalse(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) + // Interval ends at 2025-10-15, which is fully before threshold + Interval interval = new Interval("2025-10-14T00:00:00Z/2025-10-15T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-10-19T12:00:00Z/2025-10-20T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-10-19T00:00:00Z/2025-10-21T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getIoConfig_returnsConfiguredValue() + { + UserCompactionTaskIOConfig config = rule.getIoConfig(); + + Assert.assertNotNull(config); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-ioconfig-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Custom IO config", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_60_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionIOConfigRule(null, "description", PERIOD_60_DAYS, config) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionIOConfigRule("test-id", "description", null, config) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionIOConfigRule("test-id", "description", zeroPeriod, config) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); + Period negativePeriod = Period.days(-60); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionIOConfigRule("test-id", "description", negativePeriod, config) + ); + } + + @Test + public void test_constructor_nullIOConfig_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionIOConfigRule("test-id", "description", PERIOD_60_DAYS, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java new file mode 100644 index 000000000000..cb188560debb --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.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.compaction; + +import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.aggregation.LongSumAggregatorFactory; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionMetricsRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_90_DAYS = Period.days(90); + + private final AggregatorFactory[] testMetrics = new AggregatorFactory[]{ + new CountAggregatorFactory("count"), + new LongSumAggregatorFactory("total_value", "value") + }; + + private final CompactionMetricsRule rule = new CompactionMetricsRule( + "test-metrics-rule", + "Aggregate metrics for old data", + PERIOD_90_DAYS, + testMetrics + ); + + @Test + public void test_isAdditive_returnsFalse() + { + // Metrics rules are not additive - only one metrics spec can apply + Assert.assertFalse(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) + // Interval ends at 2025-09-15, which is fully before threshold + Interval interval = new Interval("2025-09-14T00:00:00Z/2025-09-15T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-09-19T12:00:00Z/2025-09-20T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-09-19T00:00:00Z/2025-09-21T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-01T00:00:00Z/2025-12-02T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getMetricsSpec_returnsConfiguredMetrics() + { + AggregatorFactory[] metrics = rule.getMetricsSpec(); + + Assert.assertNotNull(metrics); + Assert.assertEquals(2, metrics.length); + Assert.assertEquals("count", metrics[0].getName()); + Assert.assertEquals("total_value", metrics[1].getName()); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-metrics-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Aggregate metrics for old data", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_90_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionMetricsRule(null, "description", PERIOD_90_DAYS, testMetrics) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionMetricsRule("test-id", "description", null, testMetrics) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionMetricsRule("test-id", "description", zeroPeriod, testMetrics) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-90); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionMetricsRule("test-id", "description", negativePeriod, testMetrics) + ); + } + + @Test + public void test_constructor_nullMetricsSpec_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionMetricsRule("test-id", "description", PERIOD_90_DAYS, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java new file mode 100644 index 000000000000..d2761eb8a3be --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java @@ -0,0 +1,168 @@ +/* + * 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.compaction; + +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; + +public class CompactionProjectionRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_45_DAYS = Period.days(45); + + private final CompactionProjectionRule rule = new CompactionProjectionRule( + "test-projection-rule", + "Add aggregate projections", + PERIOD_45_DAYS, + Collections.emptyList() + ); + + @Test + public void test_isAdditive_returnsTrue() + { + // Projection rules are additive - multiple projections can be combined + Assert.assertTrue(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) + // Interval ends at 2025-11-01, which is fully before threshold + Interval interval = new Interval("2025-10-31T00:00:00Z/2025-11-01T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-11-03T12:00:00Z/2025-11-04T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-11-03T00:00:00Z/2025-11-05T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getProjections_returnsConfiguredValue() + { + Assert.assertNotNull(rule.getProjections()); + Assert.assertTrue(rule.getProjections().isEmpty()); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-projection-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Add aggregate projections", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_45_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionProjectionRule(null, "description", PERIOD_45_DAYS, Collections.emptyList()) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionProjectionRule("test-id", "description", null, Collections.emptyList()) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionProjectionRule("test-id", "description", zeroPeriod, Collections.emptyList()) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-45); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionProjectionRule("test-id", "description", negativePeriod, Collections.emptyList()) + ); + } + + @Test + public void test_constructor_nullProjections_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionProjectionRule("test-id", "description", PERIOD_45_DAYS, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java index 2b274e805d4c..0c0a3c8d1537 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java @@ -36,6 +36,10 @@ import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.AutoTypeColumnSchema; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.TestDataSource; @@ -45,6 +49,7 @@ import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.nested.NestedCommonFormatColumnFormatSpec; +import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -57,8 +62,14 @@ import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; public class CompactionStatusTest { @@ -884,4 +895,300 @@ private static CompactionState createCompactionStateWithGranularity(Granularity null ); } + + // ============================ + // computeRequiredSetOfFilterRulesForCandidate tests + // ============================ + + @Test + public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + NotDimFilter expectedFilter = new NotDimFilter(filterA); + + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithFilters("fp1", filterA, filterB, filterC); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + Assert.assertNull(result); + } + + @Test + public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithoutFilters("fp1"); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(3, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state = createStateWithFilters("fp1", filterA, filterB); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state1 = createStateWithFilters("fp1", filterA, filterB); + CompactionState state2 = createStateWithFilters("fp2", filterA, filterC); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state1, "fp2", state2), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state1 = createStateWithFilters("fp1", filterA); + CompactionState state2 = createStateWithFilters("fp2", filterA); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state1, "fp2", state2), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + + Assert.assertEquals(2, resultSet.size()); + Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + // No state persisted for fp1 + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void testComputeRequiredFilters_TransformSpecWithSingleFilter() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithSingleFilter("fp1", filterA); + compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(2, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_SegmentsWithNoFingerprints() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionCandidate candidate = createCandidateWithNullFingerprints(3); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + + NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + compactionStateCache + ); + + Assert.assertEquals(expectedFilter, result); + } + + // Helper methods for filter tests + + private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) + { + List segments = Arrays.stream(fingerprints) + .map(fp -> DataSegment.builder(WIKI_SEGMENT).compactionStateFingerprint(fp).build()) + .collect(Collectors.toList()); + return CompactionCandidate.from(segments, null); + } + + private CompactionCandidate createCandidateWithNullFingerprints(int count) + { + List segments = new ArrayList<>(); + for (int i = 0; i < count; i++) { + segments.add(DataSegment.builder(WIKI_SEGMENT).compactionStateFingerprint(null).build()); + } + return CompactionCandidate.from(segments, null); + } + + private CompactionState createStateWithFilters(String fingerprint, DimFilter... filters) + { + OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); + NotDimFilter notFilter = new NotDimFilter(orFilter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithSingleFilter(String fingerprint, DimFilter filter) + { + NotDimFilter notFilter = new NotDimFilter(filter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithoutFilters(String fingerprint) + { + return new CompactionState( + null, + null, + null, + null, + IndexSpec.getDefault(), + null, + null + ); + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java new file mode 100644 index 000000000000..a86718f66a10 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java @@ -0,0 +1,196 @@ +/* + * 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.compaction; + +import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class CompactionTuningConfigRuleTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final Period PERIOD_21_DAYS = Period.days(21); + + private final CompactionTuningConfigRule rule = new CompactionTuningConfigRule( + "test-tuning-rule", + "Custom tuning config", + PERIOD_21_DAYS, + createTestTuningConfig() + + ); + + @Test + public void test_isAdditive_returnsFalse() + { + // Tuning config rules are not additive - only one tuning config can apply + Assert.assertFalse(rule.isAdditive()); + } + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) + // Interval ends at 2025-11-25, which is fully before threshold + Interval interval = new Interval("2025-11-24T00:00:00Z/2025-11-25T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = new Interval("2025-11-27T12:00:00Z/2025-11-28T12:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = new Interval("2025-11-27T00:00:00Z/2025-11-29T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) + // Interval starts after threshold - NONE + Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + + CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getTuningConfig_returnsConfiguredValue() + { + UserCompactionTaskQueryTuningConfig config = rule.getTuningConfig(); + + Assert.assertNotNull(config); + Assert.assertNotNull(config.getPartitionsSpec()); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-tuning-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Custom tuning config", rule.getDescription()); + } + + @Test + public void test_getPeriod_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_21_DAYS, rule.getPeriod()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionTuningConfigRule(null, "description", PERIOD_21_DAYS, createTestTuningConfig()) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionTuningConfigRule("test-id", "description", null, createTestTuningConfig()) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionTuningConfigRule("test-id", "description", zeroPeriod, createTestTuningConfig()) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-21); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new CompactionTuningConfigRule("test-id", "description", negativePeriod, createTestTuningConfig()) + ); + } + + @Test + public void test_constructor_nullTuningConfig_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new CompactionTuningConfigRule("test-id", "description", PERIOD_21_DAYS, null) + ); + } + + private UserCompactionTaskQueryTuningConfig createTestTuningConfig() + { + return new UserCompactionTaskQueryTuningConfig( + null, + null, + null, + null, + null, + new DynamicPartitionsSpec(5000000, null), + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java new file mode 100644 index 000000000000..5cd42b112ca5 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java @@ -0,0 +1,418 @@ +/* + * 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.compaction; + +import com.google.common.collect.ImmutableList; +import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ComposingCompactionRuleProviderTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + + @Test + public void test_constructor_nullProviders_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new ComposingCompactionRuleProvider(null) + ); + } + + @Test + public void test_constructor_nullProviderInList_throwsNullPointerException() + { + List providers = new ArrayList<>(); + providers.add(createEmptyInlineProvider()); + providers.add(null); // Null provider + + NullPointerException exception = Assert.assertThrows( + NullPointerException.class, + () -> new ComposingCompactionRuleProvider(providers) + ); + + Assert.assertTrue(exception.getMessage().contains("index 1")); + } + + @Test + public void test_constructor_emptyProviderList_succeeds() + { + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + Collections.emptyList() + ); + + Assert.assertEquals("composing", composing.getType()); + Assert.assertTrue(composing.isReady()); + Assert.assertTrue(composing.getFilterRules().isEmpty()); + } + + @Test + public void test_getType_returnsComposing() + { + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(createEmptyInlineProvider()) + ); + + Assert.assertEquals("composing", composing.getType()); + } + + @Test + public void test_isReady_allProvidersReady_returnsTrue() + { + CompactionRuleProvider provider1 = createEmptyInlineProvider(); // Always ready + CompactionRuleProvider provider2 = createEmptyInlineProvider(); // Always ready + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + Assert.assertTrue(composing.isReady()); + } + + @Test + public void test_isReady_someProvidersNotReady_returnsFalse() + { + CompactionRuleProvider readyProvider = createEmptyInlineProvider(); + CompactionRuleProvider notReadyProvider = createNotReadyProvider(); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(readyProvider, notReadyProvider) + ); + + Assert.assertFalse(composing.isReady()); + } + + @Test + public void test_isReady_emptyProviderList_returnsTrue() + { + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + Collections.emptyList() + ); + + Assert.assertTrue(composing.isReady()); + } + + @Test + public void test_getFilterRules_firstWins_returnsFirstNonEmpty() + { + CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(30)); + CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(60)); + + CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + List result = composing.getFilterRules(); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", result.get(0).getId()); + } + + @Test + public void test_getFilterRules_firstProviderEmpty_returnsSecond() + { + CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(60)); + + CompactionRuleProvider emptyProvider = createEmptyInlineProvider(); + CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(emptyProvider, provider2) + ); + + List result = composing.getFilterRules(); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule2", result.get(0).getId()); + } + + @Test + public void test_getFilterRules_allProvidersEmpty_returnsEmpty() + { + CompactionRuleProvider provider1 = createEmptyInlineProvider(); + CompactionRuleProvider provider2 = createEmptyInlineProvider(); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + List result = composing.getFilterRules(); + + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void test_getGranularityRules_firstWins_returnsFirstNonEmpty() + { + CompactionGranularityRule rule1 = createGranularityRule("rule1", Period.days(7)); + CompactionGranularityRule rule2 = createGranularityRule("rule2", Period.days(30)); + + CompactionRuleProvider provider1 = createInlineProviderWithGranularityRules(ImmutableList.of(rule1)); + CompactionRuleProvider provider2 = createInlineProviderWithGranularityRules(ImmutableList.of(rule2)); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + List result = composing.getGranularityRules(); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", result.get(0).getId()); + } + + @Test + public void test_getFilterRulesWithInterval_firstWins_delegatesToFirstProvider() + { + Interval interval = new Interval("2025-11-01T00:00:00Z/2025-11-15T00:00:00Z"); + CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(30)); + + CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + CompactionRuleProvider provider2 = createEmptyInlineProvider(); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + List result = composing.getFilterRules(interval, REFERENCE_TIME); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", result.get(0).getId()); + } + + @Test + public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() + { + CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(7)); + CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(30)); + CompactionFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period + + CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2, rule3)); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + List result = composing.getCondensedAndSortedPeriods(REFERENCE_TIME); + + // Should be deduplicated and sorted: [P7D, P30D] + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Period.days(7), result.get(0)); + Assert.assertEquals(Period.days(30), result.get(1)); + } + + @Test + public void test_singleProvider_delegatesDirectly() + { + CompactionFilterRule rule = createFilterRule("rule1", Period.days(30)); + CompactionRuleProvider provider = createInlineProviderWithFilterRules(ImmutableList.of(rule)); + + ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ImmutableList.of(provider) + ); + + List result = composing.getFilterRules(); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", result.get(0).getId()); + } + + // ========== Helper Methods ========== + + private CompactionRuleProvider createEmptyInlineProvider() + { + return new InlineCompactionRuleProvider( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + } + + private CompactionRuleProvider createInlineProviderWithFilterRules(List rules) + { + return new InlineCompactionRuleProvider( + rules, + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList() + ); + } + + private CompactionRuleProvider createInlineProviderWithGranularityRules(List rules) + { + return new InlineCompactionRuleProvider( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), + rules, + Collections.emptyList() + ); + } + + private CompactionRuleProvider createNotReadyProvider() + { + return new CompactionRuleProvider() + { + @Override + public String getType() + { + return "not-ready-test-provider"; + } + + @Override + public boolean isReady() + { + return false; + } + + @Override + public List getCondensedAndSortedPeriods(DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getFilterRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getFilterRules() + { + return Collections.emptyList(); + } + + @Override + public List getMetricsRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getMetricsRules() + { + return Collections.emptyList(); + } + + @Override + public List getDimensionsRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getDimensionsRules() + { + return Collections.emptyList(); + } + + @Override + public List getIOConfigRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getIOConfigRules() + { + return Collections.emptyList(); + } + + @Override + public List getProjectionRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getProjectionRules() + { + return Collections.emptyList(); + } + + @Override + public List getGranularityRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getGranularityRules() + { + return Collections.emptyList(); + } + + @Override + public List getTuningConfigRules(Interval interval, DateTime referenceTime) + { + return Collections.emptyList(); + } + + @Override + public List getTuningConfigRules() + { + return Collections.emptyList(); + } + }; + } + + private CompactionFilterRule createFilterRule(String id, Period period) + { + return new CompactionFilterRule( + id, + "Test rule", + period, + new SelectorDimFilter("test", "value", null) + ); + } + + private CompactionGranularityRule createGranularityRule(String id, Period period) + { + return new CompactionGranularityRule( + id, + "Test granularity rule", + period, + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java new file mode 100644 index 000000000000..53b8111089f6 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java @@ -0,0 +1,308 @@ +/* + * 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.compaction; + +import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +public class InlineCompactionRuleProviderTest +{ + private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + + // Test intervals + private static final Interval INTERVAL_100_DAYS_OLD = new Interval( + "2025-09-01T00:00:00Z/2025-09-02T00:00:00Z" + ); // Ends 109 days before reference time + + private static final Interval INTERVAL_50_DAYS_OLD = new Interval( + "2025-10-20T00:00:00Z/2025-10-21T00:00:00Z" + ); // Ends 59 days before reference time + + private static final Interval INTERVAL_20_DAYS_OLD = new Interval( + "2025-11-20T00:00:00Z/2025-11-21T00:00:00Z" + ); // Ends 28 days before reference time + + private static final Interval INTERVAL_5_DAYS_OLD = new Interval( + "2025-12-13T00:00:00Z/2025-12-14T00:00:00Z" + ); // Ends 5 days before reference time + + @Test + public void test_getFilterRules_noRulesMatch_returnsEmpty() + { + CompactionFilterRule rule30d = new CompactionFilterRule( + "filter-30d", + null, + Period.days(30), + new SelectorDimFilter("dim", "val", null) + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d)).build(); + + // Interval is only 5 days old, rule requires 30 days + List result = provider.getFilterRules(INTERVAL_5_DAYS_OLD, REFERENCE_TIME); + + Assert.assertTrue("Should return empty when no rules match", result.isEmpty()); + } + + @Test + public void test_getFilterRules_oneRuleMatchesFull_returnsThatRule() + { + CompactionFilterRule rule30d = new CompactionFilterRule( + "filter-30d", + null, + Period.days(30), + new SelectorDimFilter("dim", "val", null) + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d)).build(); + + // Interval is 50 days old, rule requires 30 days - FULL match + List result = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + + Assert.assertEquals(1, result.size()); + Assert.assertEquals("filter-30d", result.get(0).getId()); + } + + @Test + public void test_getFilterRules_multipleAdditiveRulesMatchFull_returnsAll() + { + // Filter rules are additive - should return all matching rules + CompactionFilterRule rule30d = new CompactionFilterRule( + "filter-30d", + null, + Period.days(30), + new SelectorDimFilter("dim1", "val1", null) + ); + + CompactionFilterRule rule60d = new CompactionFilterRule( + "filter-60d", + null, + Period.days(60), + new SelectorDimFilter("dim2", "val2", null) + ); + + CompactionFilterRule rule90d = new CompactionFilterRule( + "filter-90d", + null, + Period.days(90), + new SelectorDimFilter("dim3", "val3", null) + ); + + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d, rule60d, rule90d)).build(); + + // Interval is 100 days old - all three rules match + List result = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + + Assert.assertEquals("Should return all matching additive rules", 3, result.size()); + Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-30d"))); + Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-60d"))); + Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-90d"))); + } + + @Test + public void test_getGranularityRules_multipleNonAdditiveRulesMatchFull_returnsOldestThreshold() + { + // Granularity rules are NOT additive - should return only the one with oldest threshold + CompactionGranularityRule rule30d = new CompactionGranularityRule( + "gran-30d", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + + CompactionGranularityRule rule60d = new CompactionGranularityRule( + "gran-60d", + null, + Period.days(60), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ); + + CompactionGranularityRule rule90d = new CompactionGranularityRule( + "gran-90d", + null, + Period.days(90), + new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule30d, rule60d, rule90d)).build(); + + // Interval is 100 days old - all three rules match FULL + // Should return rule90d because it has the oldest threshold (now - 90d) + List result = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + + Assert.assertEquals("Should return only one non-additive rule", 1, result.size()); + Assert.assertEquals("gran-90d", result.get(0).getId()); + Assert.assertEquals(Granularities.MONTH, result.get(0).getGranularityConfig().getSegmentGranularity()); + } + + @Test + public void test_getGranularityRules_partialMatchNotReturned() + { + CompactionGranularityRule rule30d = new CompactionGranularityRule( + "gran-30d", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule30d)).build(); + + // Interval is 20 days old, but rule requires 30 days + // The interval likely has PARTIAL or NONE match, not FULL + List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + + Assert.assertTrue("Should not return rules with PARTIAL match", result.isEmpty()); + } + + @Test + public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() + { + CompactionFilterRule filter30d = new CompactionFilterRule( + "f1", null, Period.days(30), new SelectorDimFilter("d", "v", null) + ); + CompactionFilterRule filter60d = new CompactionFilterRule( + "f2", null, Period.days(60), new SelectorDimFilter("d", "v", null) + ); + CompactionGranularityRule gran30d = new CompactionGranularityRule( + "g1", null, Period.days(30), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + CompactionGranularityRule gran90d = new CompactionGranularityRule( + "g2", null, Period.days(90), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(filter30d, filter60d)).granularityRules(List.of(gran30d, gran90d)).build(); + + List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); + + // Should have 3 distinct periods: P30D (appears twice), P60D, P90D + Assert.assertEquals(3, periods.size()); + // Should be sorted by duration (ascending) + Assert.assertEquals(Period.days(30), periods.get(0)); + Assert.assertEquals(Period.days(60), periods.get(1)); + Assert.assertEquals(Period.days(90), periods.get(2)); + } + + @Test + public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() + { + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(Collections.emptyList()).build(); + + List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); + + Assert.assertTrue(periods.isEmpty()); + } + + @Test + public void test_getProjectionRules_multipleAdditiveRulesMatchFull_returnsAll() + { + // Projection rules are additive + CompactionProjectionRule proj30d = new CompactionProjectionRule( + "proj-30d", null, Period.days(30), Collections.emptyList() + ); + CompactionProjectionRule proj60d = new CompactionProjectionRule( + "proj-60d", null, Period.days(60), Collections.emptyList() + ); + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().projectionRules(List.of(proj30d, proj60d)).build(); + + // Interval is 100 days old - both rules match + List result = provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + + Assert.assertEquals("Should return all matching additive projection rules", 2, result.size()); + } + + @Test + public void test_getApplicableRules_mixOfFullPartialNone_onlyReturnsFull() + { + // Create rules that will have different AppliesToMode results + CompactionGranularityRule rule10d = new CompactionGranularityRule( + "gran-10d", + null, + Period.days(10), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); // Will match FULL for 20-day-old interval + + CompactionGranularityRule rule25d = new CompactionGranularityRule( + "gran-25d", + null, + Period.days(25), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ); // Will match FULL for 20-day-old interval + + CompactionGranularityRule rule50d = new CompactionGranularityRule( + "gran-50d", + null, + Period.days(50), + new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) + ); // Will be NONE for 20-day-old interval + + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule10d, rule25d, rule50d)).build(); + + // Interval is 20 days old (ends at 2025-11-21, reference is 2025-12-19) + // rule10d: threshold = now - 10d = 2025-12-09, interval ends 2025-11-21 < 2025-12-09 -> FULL + // rule25d: threshold = now - 25d = 2025-11-24, interval ends 2025-11-21 < 2025-11-24 -> FULL + // rule50d: threshold = now - 50d = 2025-10-30, interval ends 2025-11-21 > 2025-10-30 -> NONE + // When multiple rules match, select the one with oldest threshold (smallest millis) = rule25d + List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + + // Should return rule25d (has oldest threshold among FULL matches) + Assert.assertEquals(1, result.size()); + Assert.assertEquals("gran-25d", result.get(0).getId()); + } + + @Test + public void test_constructor_nullListsDefaultToEmpty() + { + InlineCompactionRuleProvider provider = new InlineCompactionRuleProvider( + null, + null, + null, + null, + null, + null, + null + ); + + Assert.assertNotNull(provider.getFilterRules()); + Assert.assertTrue(provider.getFilterRules().isEmpty()); + Assert.assertNotNull(provider.getMetricsRules()); + Assert.assertTrue(provider.getMetricsRules().isEmpty()); + Assert.assertNotNull(provider.getGranularityRules()); + Assert.assertTrue(provider.getGranularityRules().isEmpty()); + } + + @Test + public void test_getType_returnsInline() + { + InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().build(); + + Assert.assertEquals("inline", provider.getType()); + } +} From ae21c8295f0fb58f339f3e18e0c9d5e573c18630 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 15 Jan 2026 12:22:17 -0600 Subject: [PATCH 02/90] Renaming refactor --- .../compact/CompactionSupervisorTest.java | 21 ++- .../apache/druid/guice/SupervisorModule.java | 4 +- ....java => CascadingReindexingTemplate.java} | 103 ++++++------ .../CompactionConfigBasedJobTemplate.java | 6 +- .../indexing/compact/CompactionRule.java | 2 +- .../compact/CompactionSupervisorSpec.java | 4 +- ...er.java => ReindexingConfigFinalizer.java} | 20 +-- ...nRule.java => AbstractReindexingRule.java} | 18 +-- .../server/compaction/CompactionStatus.java | 8 +- ...a => ComposingReindexingRuleProvider.java} | 60 +++---- ...java => InlineReindexingRuleProvider.java} | 108 ++++++------- ...ule.java => ReindexingDimensionsRule.java} | 4 +- ...terRule.java => ReindexingFilterRule.java} | 4 +- ...le.java => ReindexingGranularityRule.java} | 4 +- ...gRule.java => ReindexingIOConfigRule.java} | 4 +- ...csRule.java => ReindexingMetricsRule.java} | 4 +- ...ule.java => ReindexingProjectionRule.java} | 4 +- ...ompactionRule.java => ReindexingRule.java} | 6 +- ...vider.java => ReindexingRuleProvider.java} | 80 +++++----- ...e.java => ReindexingTuningConfigRule.java} | 4 +- .../compaction/CompactionStatusTest.java | 40 ++--- ... ComposingReindexingRuleProviderTest.java} | 150 +++++++++--------- ... => InlineReindexingRuleProviderTest.java} | 74 ++++----- ...java => ReindexingDimensionsRuleTest.java} | 30 ++-- ...est.java => ReindexingFilterRuleTest.java} | 58 +++---- ...ava => ReindexingGranularityRuleTest.java} | 30 ++-- ...t.java => ReindexingIOConfigRuleTest.java} | 30 ++-- ...st.java => ReindexingMetricsRuleTest.java} | 30 ++-- ...java => ReindexingProjectionRuleTest.java} | 30 ++-- ...va => ReindexingTuningConfigRuleTest.java} | 30 ++-- 30 files changed, 487 insertions(+), 483 deletions(-) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{CascadingCompactionTemplate.java => CascadingReindexingTemplate.java} (80%) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{CompactionConfigFinalizer.java => ReindexingConfigFinalizer.java} (71%) rename server/src/main/java/org/apache/druid/server/compaction/{AbstractCompactionRule.java => AbstractReindexingRule.java} (90%) rename server/src/main/java/org/apache/druid/server/compaction/{ComposingCompactionRuleProvider.java => ComposingReindexingRuleProvider.java} (81%) rename server/src/main/java/org/apache/druid/server/compaction/{InlineCompactionRuleProvider.java => InlineReindexingRuleProvider.java} (76%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionDimensionsRule.java => ReindexingDimensionsRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionFilterRule.java => ReindexingFilterRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionGranularityRule.java => ReindexingGranularityRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionIOConfigRule.java => ReindexingIOConfigRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionMetricsRule.java => ReindexingMetricsRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionProjectionRule.java => ReindexingProjectionRule.java} (96%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionRule.java => ReindexingRule.java} (94%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionRuleProvider.java => ReindexingRuleProvider.java} (73%) rename server/src/main/java/org/apache/druid/server/compaction/{CompactionTuningConfigRule.java => ReindexingTuningConfigRule.java} (96%) rename server/src/test/java/org/apache/druid/server/compaction/{ComposingCompactionRuleProviderTest.java => ComposingReindexingRuleProviderTest.java} (62%) rename server/src/test/java/org/apache/druid/server/compaction/{InlineCompactionRuleProviderTest.java => InlineReindexingRuleProviderTest.java} (80%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionDimensionsRuleTest.java => ReindexingDimensionsRuleTest.java} (83%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionFilterRuleTest.java => ReindexingFilterRuleTest.java} (84%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionGranularityRuleTest.java => ReindexingGranularityRuleTest.java} (84%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionIOConfigRuleTest.java => ReindexingIOConfigRuleTest.java} (83%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionMetricsRuleTest.java => ReindexingMetricsRuleTest.java} (84%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionProjectionRuleTest.java => ReindexingProjectionRuleTest.java} (82%) rename server/src/test/java/org/apache/druid/server/compaction/{CompactionTuningConfigRuleTest.java => ReindexingTuningConfigRuleTest.java} (84%) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index bc1158c3e858..1abe8342c205 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -25,7 +25,7 @@ import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; import org.apache.druid.indexing.common.task.IndexTask; -import org.apache.druid.indexing.compact.CascadingCompactionTemplate; +import org.apache.druid.indexing.compact.CascadingReindexingTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.Segments; import org.apache.druid.java.util.common.DateTimes; @@ -38,10 +38,9 @@ import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; -import org.apache.druid.server.compaction.CompactionGranularityRule; -import org.apache.druid.server.compaction.CompactionStatus; -import org.apache.druid.server.compaction.CompactionTuningConfigRule; -import org.apache.druid.server.compaction.InlineCompactionRuleProvider; +import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingGranularityRule; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; @@ -314,20 +313,20 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ); Assertions.assertEquals(16, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); - CompactionGranularityRule hourRule = new CompactionGranularityRule( + ReindexingGranularityRule hourRule = new ReindexingGranularityRule( "hourRule", "Compact to HOUR granularity for data older than 1 days", Period.days(1), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); - CompactionGranularityRule dayRule = new CompactionGranularityRule( + ReindexingGranularityRule dayRule = new ReindexingGranularityRule( "dayRule", "Compact to DAY granularity for data older than 2 days", Period.days(7), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) ); - CompactionTuningConfigRule tuningConfigRule = new CompactionTuningConfigRule( + ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( "tuningConfigRule", "Use dimension range partitioning with max 1000 rows per segment", Period.days(1), @@ -354,13 +353,13 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ) ); - InlineCompactionRuleProvider ruleProvider = InlineCompactionRuleProvider.builder() + InlineReindexingRuleProvider ruleProvider = InlineReindexingRuleProvider.builder() .granularityRules(List.of(hourRule, dayRule)) .tuningConfigRules(List.of(tuningConfigRule)) .build(); - CascadingCompactionTemplate cascadingCompactionTemplate = new CascadingCompactionTemplate(dataSource, ruleProvider); - runCompactionWithSpec(cascadingCompactionTemplate); + CascadingReindexingTemplate cascadingReindexingTemplate = new CascadingReindexingTemplate(dataSource, ruleProvider); + runCompactionWithSpec(cascadingReindexingTemplate); waitForAllCompactionTasksToFinish(); Assertions.assertEquals(4, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); diff --git a/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java b/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java index 03124e98e02f..12730c25b950 100644 --- a/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java +++ b/indexing-service/src/main/java/org/apache/druid/guice/SupervisorModule.java @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.google.common.collect.ImmutableList; import com.google.inject.Binder; -import org.apache.druid.indexing.compact.CascadingCompactionTemplate; +import org.apache.druid.indexing.compact.CascadingReindexingTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.supervisor.SupervisorStateManagerConfig; import org.apache.druid.indexing.scheduledbatch.ScheduledBatchSupervisorSpec; @@ -49,7 +49,7 @@ public List getJacksonModules() .registerSubtypes( new NamedType(CompactionSupervisorSpec.class, CompactionSupervisorSpec.TYPE), new NamedType(ScheduledBatchSupervisorSpec.class, ScheduledBatchSupervisorSpec.TYPE), - new NamedType(CascadingCompactionTemplate.class, CascadingCompactionTemplate.TYPE) + new NamedType(CascadingReindexingTemplate.class, CascadingReindexingTemplate.TYPE) ) ); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java similarity index 80% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index e82c66712322..a01e5114ab61 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingCompactionTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -32,15 +32,16 @@ import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.segment.transform.CompactionTransformSpec; -import org.apache.druid.server.compaction.CompactionDimensionsRule; -import org.apache.druid.server.compaction.CompactionFilterRule; -import org.apache.druid.server.compaction.CompactionGranularityRule; -import org.apache.druid.server.compaction.CompactionIOConfigRule; -import org.apache.druid.server.compaction.CompactionMetricsRule; -import org.apache.druid.server.compaction.CompactionProjectionRule; -import org.apache.druid.server.compaction.CompactionRuleProvider; import org.apache.druid.server.compaction.CompactionStatus; -import org.apache.druid.server.compaction.CompactionTuningConfigRule; +import org.apache.druid.server.compaction.ReindexingDimensionsRule; +import org.apache.druid.server.compaction.ReindexingFilterRule; +import org.apache.druid.server.compaction.ReindexingGranularityRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; +import org.apache.druid.server.compaction.ReindexingMetricsRule; +import org.apache.druid.server.compaction.ReindexingProjectionRule; +import org.apache.druid.server.compaction.ReindexingRule; +import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -60,8 +61,8 @@ import java.util.stream.Collectors; /** - * Template to perform period-based cascading compaction. Contains a list of - * {@link org.apache.druid.server.compaction.CompactionRule} which divide the segment timeline into compactible + * Template to perform period-based cascading reindexing. Contains a list of + * {@link ReindexingRule} which divide the segment timeline into reindexable * intervals. Each rule specifies a period relative to the current time which is * used to determine its applicable interval: *

      @@ -72,23 +73,23 @@ *
    * * If two adjacent rules explicitly specify a segment granularity, the boundary - * between them may be adjusted to ensure that there are no uncompacted gaps in the timeline. + * between them may be adjusted to ensure that there are no unprocessed gaps in the timeline. *

    * This template never needs to be deserialized as a {@code BatchIndexingJobTemplate} */ -public class CascadingCompactionTemplate implements CompactionJobTemplate, DataSourceCompactionConfig +public class CascadingReindexingTemplate implements CompactionJobTemplate, DataSourceCompactionConfig { - private static final Logger LOG = new Logger(CascadingCompactionTemplate.class); + private static final Logger LOG = new Logger(CascadingReindexingTemplate.class); - public static final String TYPE = "compactCascade"; + public static final String TYPE = "reindexCascade"; private final String dataSource; - private final CompactionRuleProvider ruleProvider; + private final ReindexingRuleProvider ruleProvider; @JsonCreator - public CascadingCompactionTemplate( + public CascadingReindexingTemplate( @JsonProperty("dataSource") String dataSource, - @JsonProperty("ruleProvider") CompactionRuleProvider ruleProvider + @JsonProperty("ruleProvider") ReindexingRuleProvider ruleProvider ) { this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); @@ -103,15 +104,15 @@ public String getDataSource() } /** - * Creates a config finalizer that optimizes filter rules for cascading compaction. - * When a candidate segment has already been compacted with a subset of filter rules, + * Creates a config finalizer that optimizes filter rules for cascading reindexing. + * When a candidate segment has already been reindexed with a subset of filter rules, * this finalizer computes the minimal set of additional filter rules needed. - * This optimization reduces bitmap operations during compaction. + * This optimization reduces bitmap operations during reindexing. */ - private static CompactionConfigFinalizer createCascadingFinalizer() + private static ReindexingConfigFinalizer createCascadingFinalizer() { return (config, candidate, params) -> { - // Only optimize if candidate has been compacted before and config has a NotDimFilter + // Only optimize if candidate has been reindexed before and config has a NotDimFilter if (candidate.getCurrentStatus() != null && !candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON) && config.getTransformSpec() != null && @@ -122,7 +123,7 @@ private static CompactionConfigFinalizer createCascadingFinalizer() NotDimFilter reducedTransformSpecFilter = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, (NotDimFilter) config.getTransformSpec().getFilter(), - params.getCompactionStateCache() + params.getFingerprintMapper() ); // Safe cast: we know this is InlineSchemaDataSourceCompactionConfig because we just built it @@ -144,7 +145,7 @@ public List createCompactionJobs( // Check if the rule provider is ready before attempting to create jobs if (!ruleProvider.isReady()) { LOG.info( - "Rule provider [%s] is not ready, skipping compaction job creation for dataSource[%s]", + "Rule provider [%s] is not ready, skipping reindexing job creation for dataSource[%s]", ruleProvider.getType(), dataSource ); @@ -161,26 +162,26 @@ public List createCompactionJobs( // Generate intervals from periods and create jobs for each List intervals = generateIntervalsFromPeriods(sortedPeriods, currentTime, ruleProvider.getGranularityRules()); - for (Interval compactionInterval : intervals) { + for (Interval reindexingInterval : intervals) { InlineSchemaDataSourceCompactionConfig.Builder builder = InlineSchemaDataSourceCompactionConfig.builder() .forDataSource(dataSource) .withSkipOffsetFromLatest(Period.ZERO); - // Apply all applicable compaction rules to the builder - int ruleCount = applyRulesToBuilder(builder, compactionInterval, currentTime); + // Apply all applicable reindexing rules to the builder + int ruleCount = applyRulesToBuilder(builder, reindexingInterval, currentTime); if (ruleCount > 0) { - LOG.info("Creating compaction jobs for interval[%s] with %d rules", compactionInterval, ruleCount); + LOG.info("Creating reindexing jobs for interval[%s] with %d rules", reindexingInterval, ruleCount); allJobs.addAll( createJobsForSearchInterval( new CompactionConfigBasedJobTemplate(builder.build(), createCascadingFinalizer()), - compactionInterval, + reindexingInterval, source, jobParams ) ); } else { - LOG.info("No applicable compaction rules found for interval[%s]", compactionInterval); + LOG.info("No applicable reindexing rules found for interval[%s]", reindexingInterval); } } return allJobs; @@ -212,7 +213,7 @@ public List createCompactionJobs( * and the HOUR-granularity rule starts cleanly at a day boundary. * */ - private List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) + private List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) { List intervals = new ArrayList<>(); DateTime previousAdjustedBoundary = null; @@ -229,12 +230,12 @@ private List generateIntervalsFromPeriods(List sortedPeriods, // We may need to adjust the start time to avoid gaps if both adjacent rules have segment granularities defined. final DateTime calculatedStartTime = referenceTime.minus(sortedPeriods.get(i + 1)); final int finalI = i; - CompactionGranularityRule currentRule = granularityRules.stream() + ReindexingGranularityRule currentRule = granularityRules.stream() .filter(rule -> rule.getPeriod().equals(sortedPeriods.get(finalI)) && rule.getGranularityConfig().getSegmentGranularity() != null) .findFirst() .orElse(null); - CompactionGranularityRule beforeRule = granularityRules.stream() + ReindexingGranularityRule beforeRule = granularityRules.stream() .filter(rule -> rule.getPeriod().equals(sortedPeriods.get(finalI + 1)) && rule.getGranularityConfig().getSegmentGranularity() != null) .findFirst() @@ -267,62 +268,62 @@ private List generateIntervalsFromPeriods(List sortedPeriods, } /** - * Applies all applicable compaction rules to the builder for the given interval. + * Applies all applicable reindexing rules to the builder for the given interval. * * @return number of rules applied */ private int applyRulesToBuilder( InlineSchemaDataSourceCompactionConfig.Builder builder, - Interval compactionInterval, + Interval reindexingInterval, DateTime referenceTime ) { int ruleCount = 0; // Granularity rules (non-additive, take first) - List granularityRules = ruleProvider.getGranularityRules(compactionInterval, referenceTime); + List granularityRules = ruleProvider.getGranularityRules(reindexingInterval, referenceTime); if (!granularityRules.isEmpty()) { - LOG.info("Applying granularity rule %s for interval %s", granularityRules.get(0).getId(), compactionInterval); + LOG.info("Applying granularity rule %s for interval %s", granularityRules.get(0).getId(), reindexingInterval); builder.withGranularitySpec(granularityRules.get(0).getGranularityConfig()); ruleCount += 1; } // Tuning config rules (non-additive, take first) - List tuningConfigRules = ruleProvider.getTuningConfigRules(compactionInterval, referenceTime); + List tuningConfigRules = ruleProvider.getTuningConfigRules(reindexingInterval, referenceTime); if (!tuningConfigRules.isEmpty()) { - LOG.info("Applying tuning config rule %s for interval %s", tuningConfigRules.get(0).getId(), compactionInterval); + LOG.info("Applying tuning config rule %s for interval %s", tuningConfigRules.get(0).getId(), reindexingInterval); builder.withTuningConfig(tuningConfigRules.get(0).getTuningConfig()); ruleCount += 1; } // Metrics rules (non-additive, take first) - List metricsRules = ruleProvider.getMetricsRules(compactionInterval, referenceTime); + List metricsRules = ruleProvider.getMetricsRules(reindexingInterval, referenceTime); if (!metricsRules.isEmpty()) { - LOG.info("Applying metrics rule %s for interval %s", metricsRules.get(0).getId(), compactionInterval); + LOG.info("Applying metrics rule %s for interval %s", metricsRules.get(0).getId(), reindexingInterval); builder.withMetricsSpec(metricsRules.get(0).getMetricsSpec()); ruleCount += 1; } // Dimensions rules (non-additive, take first) - List dimensionsRules = ruleProvider.getDimensionsRules(compactionInterval, referenceTime); + List dimensionsRules = ruleProvider.getDimensionsRules(reindexingInterval, referenceTime); if (!dimensionsRules.isEmpty()) { - LOG.info("Applying dimensions rule %s for interval %s", dimensionsRules.get(0).getId(), compactionInterval); + LOG.info("Applying dimensions rule %s for interval %s", dimensionsRules.get(0).getId(), reindexingInterval); builder.withDimensionsSpec(dimensionsRules.get(0).getDimensionsSpec()); ruleCount += 1; } // IO config rules (non-additive, take first) - List ioConfigRules = ruleProvider.getIOConfigRules(compactionInterval, referenceTime); + List ioConfigRules = ruleProvider.getIOConfigRules(reindexingInterval, referenceTime); if (!ioConfigRules.isEmpty()) { - LOG.info("Applying IO config rule %s for interval %s", ioConfigRules.get(0).getId(), compactionInterval); + LOG.info("Applying IO config rule %s for interval %s", ioConfigRules.get(0).getId(), reindexingInterval); builder.withIoConfig(ioConfigRules.get(0).getIoConfig()); ruleCount += 1; } // Projection rules (additive, combine all) - List projectionRules = ruleProvider.getProjectionRules(compactionInterval, referenceTime); + List projectionRules = ruleProvider.getProjectionRules(reindexingInterval, referenceTime); if (!projectionRules.isEmpty()) { - LOG.info("Applying [%d] projection rules for interval %s", projectionRules.size(), compactionInterval); + LOG.info("Applying [%d] projection rules for interval %s", projectionRules.size(), reindexingInterval); builder.withProjections( projectionRules.stream() .flatMap(rule -> rule.getProjections().stream()) @@ -332,11 +333,11 @@ private int applyRulesToBuilder( } // Filter rules (additive, combine with OR and wrap in NOT) - List filterRules = ruleProvider.getFilterRules(compactionInterval, referenceTime); + List filterRules = ruleProvider.getFilterRules(reindexingInterval, referenceTime); if (!filterRules.isEmpty()) { - LOG.info("Applying up to [%d] filter rules for interval %s", filterRules.size(), compactionInterval); + LOG.info("Applying up to [%d] filter rules for interval %s", filterRules.size(), reindexingInterval); List removeConditions = filterRules.stream() - .map(CompactionFilterRule::getFilter) + .map(ReindexingFilterRule::getFilter) .collect(Collectors.toList()); DimFilter removeFilter = removeConditions.size() == 1 @@ -467,7 +468,7 @@ public AggregatorFactory[] getMetricsSpec() } @JsonProperty - private CompactionRuleProvider getRuleProvider() + private ReindexingRuleProvider getRuleProvider() { return ruleProvider; } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java index 5089fcd270d0..e29f795b4b34 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java @@ -48,16 +48,16 @@ public class CompactionConfigBasedJobTemplate implements CompactionJobTemplate { private final DataSourceCompactionConfig config; - private final CompactionConfigFinalizer configFinalizer; + private final ReindexingConfigFinalizer configFinalizer; public CompactionConfigBasedJobTemplate(DataSourceCompactionConfig config) { - this(config, CompactionConfigFinalizer.IDENTITY); + this(config, ReindexingConfigFinalizer.IDENTITY); } public CompactionConfigBasedJobTemplate( DataSourceCompactionConfig config, - CompactionConfigFinalizer configFinalizer + ReindexingConfigFinalizer configFinalizer ) { this.config = config; diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java index 10099566ea71..f73a488821c2 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java @@ -26,7 +26,7 @@ import org.joda.time.Period; /** - * A single rule used inside {@link CascadingCompactionTemplate}. + * A single rule used inside {@link CascadingReindexingTemplate}. */ public class CompactionRule { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java index 998d7e0c7e67..d9002655f742 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisorSpec.java @@ -98,8 +98,8 @@ public CompactionSupervisor createSupervisor() */ public CompactionJobTemplate getTemplate() { - if (spec instanceof CascadingCompactionTemplate) { - return (CascadingCompactionTemplate) spec; + if (spec instanceof CascadingReindexingTemplate) { + return (CascadingReindexingTemplate) spec; } else { return new CompactionConfigBasedJobTemplate(spec); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java similarity index 71% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java index bc0b7bca087f..a432705fe3ca 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigFinalizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java @@ -24,22 +24,22 @@ /** * Functional interface for customizing a {@link DataSourceCompactionConfig} for a specific - * {@link CompactionCandidate} before creating a compaction job. This allows template-specific + * {@link CompactionCandidate} before creating a reindexing job. This allows template-specific * logic to be injected without hardcoding behavior in {@link CompactionConfigBasedJobTemplate}. *

    - * For example, cascading compaction templates can use this to optimize filter rules based on - * the candidate's compaction state, while simpler templates can use the identity finalizer. + * For example, cascading reindexing templates can use this to optimize filter rules based on + * the candidate's indexing state, while simpler templates can use the identity finalizer. */ @FunctionalInterface -public interface CompactionConfigFinalizer +public interface ReindexingConfigFinalizer { /** - * Customize the compaction config for a specific candidate. + * Customize the reindexing config for a specific candidate. * - * @param config the base compaction config - * @param candidate the segment candidate being compacted - * @param params the compaction job parameters - * @return the finalized config to use for this candidate (may be the same as input or a modified version) + * @param config the base reindexing config + * @param candidate the segment candidate being reindexed + * @param params the reindexing job parameters + * @return the finalized config to use for this candidate (this may be the same as input or a modified version) */ DataSourceCompactionConfig finalizeConfig( DataSourceCompactionConfig config, @@ -51,5 +51,5 @@ DataSourceCompactionConfig finalizeConfig( * Identity finalizer that returns the config unchanged. * Use this for templates that don't need per-candidate customization. */ - CompactionConfigFinalizer IDENTITY = (config, candidate, params) -> config; + ReindexingConfigFinalizer IDENTITY = (config, candidate, params) -> config; } diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java similarity index 90% rename from server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java rename to server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index 4e9ff2a44f1a..a6321cc62149 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractCompactionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -31,25 +31,25 @@ import java.util.Objects; /** - * Base implementation for compaction rules that apply based on data age thresholds. + * Base implementation for reindexing rules that apply based on data age thresholds. *

    * Provides period-based applicability logic: a rule with period P7D applies to data - * older than 7 days. Subclasses define specific compaction configuration (granularity, + * older than 7 days. Subclasses define specific reindexing configuration (granularity, * filters, tuning, etc.) and whether multiple rules can combine (additive vs non-additive). *

    * The {@link #appliesTo(Interval, DateTime)} method determines if an interval is fully, - * partially, or not covered by this rule's threshold, enabling cascading compaction + * partially, or not covered by this rule's threshold, enabling cascading reindexing * strategies where different rules apply to different age tiers of data. */ -public abstract class AbstractCompactionRule implements CompactionRule +public abstract class AbstractReindexingRule implements ReindexingRule { - private static final Logger LOG = new Logger(AbstractCompactionRule.class); + private static final Logger LOG = new Logger(AbstractReindexingRule.class); private final String id; private final String description; private final Period period; - public AbstractCompactionRule( + public AbstractReindexingRule( @Nonnull String id, @Nullable String description, @Nonnull Period period @@ -144,13 +144,13 @@ public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTi DateTime threshold = now.minus(period); if (intervalEnd.isBefore(threshold) || intervalEnd.isEqual(threshold)) { - LOG.debug("Compaction rule [%s] applies FULLY to interval [%s]. Threshold: [%s]", id, interval, threshold); + LOG.debug("Reindexing rule [%s] applies FULLY to interval [%s]. Threshold: [%s]", id, interval, threshold); return AppliesToMode.FULL; } else if (intervalStart.isAfter(threshold)) { - LOG.debug("Compaction rule [%s] does NOT apply to interval [%s]. Threshold: [%s]", id, interval, threshold); + LOG.debug("Reindexing rule [%s] does NOT apply to interval [%s]. Threshold: [%s]", id, interval, threshold); return AppliesToMode.NONE; } else { - LOG.debug("Compaction rule [%s] applies PARTIALLY to interval [%s]. Threshold: [%s]", id, interval, threshold); + LOG.debug("Reindexing rule [%s] applies PARTIALLY to interval [%s]. Threshold: [%s]", id, interval, threshold); return AppliesToMode.PARTIAL; } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 6e79488c70b7..73806a4a2863 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -267,14 +267,14 @@ public static CompactionStatus running(String message) * * @param candidateSegments * @param expectedFilter - * @param compactionStateCache + * @param fingerprintMapper * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied */ @Nullable public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( CompactionCandidate candidateSegments, NotDimFilter expectedFilter, - CompactionStateCache compactionStateCache + IndexingStateFingerprintMapper fingerprintMapper ) { if (!(expectedFilter.getField() instanceof OrDimFilter)) { @@ -285,7 +285,7 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( // Collect unique fingerprints Set uniqueFingerprints = candidateSegments.getSegments().stream() - .map(DataSegment::getCompactionStateFingerprint) + .map(DataSegment::getIndexingStateFingerprint) .filter(Objects::nonNull) .collect(Collectors.toSet()); @@ -298,7 +298,7 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( Set unappliedRules = new HashSet<>(); for (String fingerprint : uniqueFingerprints) { - CompactionState state = compactionStateCache.getCompactionStateByFingerprint(fingerprint).orElse(null); + CompactionState state = fingerprintMapper.getStateForFingerprint(fingerprint).orElse(null); if (state == null) { // Safety: if state is missing, return all filters eagerly since we can't determine applied filters diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java similarity index 81% rename from server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java rename to server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 3bb35bc3a26c..61cec84e1ecf 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingCompactionRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -32,7 +32,7 @@ import java.util.stream.Collectors; /** - * A meta-provider that composes multiple {@link CompactionRuleProvider}s with first-wins semantics. + * A meta-provider that composes multiple {@link ReindexingRuleProvider}s with first-wins semantics. *

    * This provider delegates rule queries to a list of child providers in order. For each rule type, * it returns the result from the first provider that has non-empty rules of that type. @@ -85,15 +85,15 @@ *

  • Filter rules come from the second provider (first provider with filters)
  • *
*/ -public class ComposingCompactionRuleProvider implements CompactionRuleProvider +public class ComposingReindexingRuleProvider implements ReindexingRuleProvider { public static final String TYPE = "composing"; - private final List providers; + private final List providers; @JsonCreator - public ComposingCompactionRuleProvider( - @JsonProperty("providers") List providers + public ComposingReindexingRuleProvider( + @JsonProperty("providers") List providers ) { this.providers = Objects.requireNonNull(providers, "providers cannot be null"); @@ -107,7 +107,7 @@ public ComposingCompactionRuleProvider( } @JsonProperty("providers") - public List getProviders() + public List getProviders() { return providers; } @@ -122,7 +122,7 @@ public String getType() public boolean isReady() { // All providers must be ready - return providers.stream().allMatch(CompactionRuleProvider::isReady); + return providers.stream().allMatch(ReindexingRuleProvider::isReady); } @Override @@ -137,17 +137,17 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) } @Override - public List getFilterRules() + public List getFilterRules() { return providers.stream() - .map(CompactionRuleProvider::getFilterRules) + .map(ReindexingRuleProvider::getFilterRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getFilterRules(Interval interval, DateTime referenceTime) + public List getFilterRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getFilterRules(interval, referenceTime)) @@ -157,17 +157,17 @@ public List getFilterRules(Interval interval, DateTime ref } @Override - public List getMetricsRules() + public List getMetricsRules() { return providers.stream() - .map(CompactionRuleProvider::getMetricsRules) + .map(ReindexingRuleProvider::getMetricsRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) + public List getMetricsRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getMetricsRules(interval, referenceTime)) @@ -177,17 +177,17 @@ public List getMetricsRules(Interval interval, DateTime r } @Override - public List getDimensionsRules() + public List getDimensionsRules() { return providers.stream() - .map(CompactionRuleProvider::getDimensionsRules) + .map(ReindexingRuleProvider::getDimensionsRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) + public List getDimensionsRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getDimensionsRules(interval, referenceTime)) @@ -197,17 +197,17 @@ public List getDimensionsRules(Interval interval, Date } @Override - public List getIOConfigRules() + public List getIOConfigRules() { return providers.stream() - .map(CompactionRuleProvider::getIOConfigRules) + .map(ReindexingRuleProvider::getIOConfigRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) + public List getIOConfigRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getIOConfigRules(interval, referenceTime)) @@ -217,17 +217,17 @@ public List getIOConfigRules(Interval interval, DateTime } @Override - public List getProjectionRules() + public List getProjectionRules() { return providers.stream() - .map(CompactionRuleProvider::getProjectionRules) + .map(ReindexingRuleProvider::getProjectionRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) + public List getProjectionRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getProjectionRules(interval, referenceTime)) @@ -237,17 +237,17 @@ public List getProjectionRules(Interval interval, Date } @Override - public List getGranularityRules() + public List getGranularityRules() { return providers.stream() - .map(CompactionRuleProvider::getGranularityRules) + .map(ReindexingRuleProvider::getGranularityRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) + public List getGranularityRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getGranularityRules(interval, referenceTime)) @@ -257,17 +257,17 @@ public List getGranularityRules(Interval interval, Da } @Override - public List getTuningConfigRules() + public List getTuningConfigRules() { return providers.stream() - .map(CompactionRuleProvider::getTuningConfigRules) + .map(ReindexingRuleProvider::getTuningConfigRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) + public List getTuningConfigRules(Interval interval, DateTime referenceTime) { return providers.stream() .map(p -> p.getTuningConfigRules(interval, referenceTime)) @@ -285,7 +285,7 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) { return false; } - ComposingCompactionRuleProvider that = (ComposingCompactionRuleProvider) o; + ComposingReindexingRuleProvider that = (ComposingReindexingRuleProvider) o; return Objects.equals(providers, that.providers); } @@ -298,7 +298,7 @@ public int hashCode() @Override public String toString() { - return "ComposingCompactionRuleProvider{" + + return "ComposingReindexingRuleProvider{" + "providers=" + providers + ", ready=" + isReady() + '}'; diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java similarity index 76% rename from server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java rename to server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index c58af2f694af..f73840ee1804 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineCompactionRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -42,8 +42,8 @@ * This is the simplest provider implementation, suitable for testing and use cases where the number of rules is * relatively small and can be defined directly in the compaction config. *

- * When filtering rules by interval, this provider only returns rules where {@link CompactionRule#appliesTo(Interval, DateTime)} - * returns {@link CompactionRule.AppliesToMode#FULL}. Rules with partial or no overlap are excluded. + * When filtering rules by interval, this provider only returns rules where {@link ReindexingRule#appliesTo(Interval, DateTime)} + * returns {@link ReindexingRule.AppliesToMode#FULL}. Rules with partial or no overlap are excluded. *

* For non-additive rule types, when multiple rules fully match an interval, only the rule with the oldest threshold * (largest period) is returned. For example, if both a P30D and P90D granularity rule match an interval, the P90D @@ -84,28 +84,28 @@ * } * } */ -public class InlineCompactionRuleProvider implements CompactionRuleProvider +public class InlineReindexingRuleProvider implements ReindexingRuleProvider { public static final String TYPE = "inline"; - private final List compactionFilterRules; - private final List compactionMetricsRules; - private final List compactionDimensionsRules; - private final List compactionIOConfigRules; - private final List compactionProjectionRules; - private final List compactionGranularityRules; - private final List compactionTuningConfigRules; + private final List compactionFilterRules; + private final List compactionMetricsRules; + private final List compactionDimensionsRules; + private final List compactionIOConfigRules; + private final List compactionProjectionRules; + private final List compactionGranularityRules; + private final List compactionTuningConfigRules; @JsonCreator - public InlineCompactionRuleProvider( - @JsonProperty("compactionFilterRules") @Nullable List compactionFilterRules, - @JsonProperty("compactionMetricsRules") @Nullable List compactionMetricsRules, - @JsonProperty("compactionDimensionsRules") @Nullable List compactionDimensionsRules, - @JsonProperty("compactionIOConfigRules") @Nullable List compactionIOConfigRules, - @JsonProperty("compactionProjectionRules") @Nullable List compactionProjectionRules, - @JsonProperty("compactionGranularityRules") @Nullable List compactionGranularityRules, - @JsonProperty("compactionTuningConfigRules") @Nullable List compactionTuningConfigRules + public InlineReindexingRuleProvider( + @JsonProperty("compactionFilterRules") @Nullable List compactionFilterRules, + @JsonProperty("compactionMetricsRules") @Nullable List compactionMetricsRules, + @JsonProperty("compactionDimensionsRules") @Nullable List compactionDimensionsRules, + @JsonProperty("compactionIOConfigRules") @Nullable List compactionIOConfigRules, + @JsonProperty("compactionProjectionRules") @Nullable List compactionProjectionRules, + @JsonProperty("compactionGranularityRules") @Nullable List compactionGranularityRules, + @JsonProperty("compactionTuningConfigRules") @Nullable List compactionTuningConfigRules ) { this.compactionFilterRules = Configs.valueOrDefault(compactionFilterRules, Collections.emptyList()); @@ -131,49 +131,49 @@ public String getType() @Override @JsonProperty("compactionFilterRules") - public List getFilterRules() + public List getFilterRules() { return compactionFilterRules; } @Override @JsonProperty("compactionMetricsRules") - public List getMetricsRules() + public List getMetricsRules() { return compactionMetricsRules; } @Override @JsonProperty("compactionDimensionsRules") - public List getDimensionsRules() + public List getDimensionsRules() { return compactionDimensionsRules; } @Override @JsonProperty("compactionIOConfigRules") - public List getIOConfigRules() + public List getIOConfigRules() { return compactionIOConfigRules; } @Override @JsonProperty("compactionProjectionRules") - public List getProjectionRules() + public List getProjectionRules() { return compactionProjectionRules; } @Override @JsonProperty("compactionGranularityRules") - public List getGranularityRules() + public List getGranularityRules() { return compactionGranularityRules; } @Override @JsonProperty("compactionTuningConfigRules") - public List getTuningConfigRules() + public List getTuningConfigRules() { return compactionTuningConfigRules; } @@ -191,7 +191,7 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) compactionTuningConfigRules ) .flatMap(List::stream) - .map(CompactionRule::getPeriod) + .map(ReindexingRule::getPeriod) .distinct() .sorted(Comparator.comparingLong(period -> { DateTime endTime = referenceTime.plus(period); @@ -202,43 +202,43 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) } @Override - public List getFilterRules(Interval interval, DateTime referenceTime) + public List getFilterRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionFilterRules, interval, referenceTime); } @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) + public List getMetricsRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionMetricsRules, interval, referenceTime); } @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) + public List getDimensionsRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionDimensionsRules, interval, referenceTime); } @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) + public List getIOConfigRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionIOConfigRules, interval, referenceTime); } @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) + public List getProjectionRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionProjectionRules, interval, referenceTime); } @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) + public List getGranularityRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionGranularityRules, interval, referenceTime); } @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) + public List getTuningConfigRules(Interval interval, DateTime referenceTime) { return getApplicableRules(compactionTuningConfigRules, interval, referenceTime); } @@ -251,13 +251,13 @@ public List getTuningConfigRules(Interval interval, * Any non-additive rule types will only return a single rule, even if multiple rules fully apply to the interval. The * interval returned is the one with the oldest threshold (i.e., the largest period into the past from "now"). */ - private List getApplicableRules(List rules, Interval interval, DateTime referenceTime) + private List getApplicableRules(List rules, Interval interval, DateTime referenceTime) { boolean areRulesAdditive = false; List applicableRules = new ArrayList<>(); for (T rule : rules) { areRulesAdditive = rule.isAdditive(); - if (rule.appliesTo(interval, referenceTime) == CompactionRule.AppliesToMode.FULL) { + if (rule.appliesTo(interval, referenceTime) == ReindexingRule.AppliesToMode.FULL) { applicableRules.add(rule); } } @@ -284,7 +284,7 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) { return false; } - InlineCompactionRuleProvider that = (InlineCompactionRuleProvider) o; + InlineReindexingRuleProvider that = (InlineReindexingRuleProvider) o; return Objects.equals(compactionFilterRules, that.compactionFilterRules) && Objects.equals(compactionMetricsRules, that.compactionMetricsRules) && Objects.equals(compactionDimensionsRules, that.compactionDimensionsRules) @@ -311,7 +311,7 @@ public int hashCode() @Override public String toString() { - return "InlineCompactionRuleProvider{" + return "InlineReindexingRuleProvider{" + "compactionFilterRules=" + compactionFilterRules + ", compactionMetricsRules=" + compactionMetricsRules + ", compactionDimensionsRules=" + compactionDimensionsRules @@ -324,59 +324,59 @@ public String toString() public static class Builder { - private List compactionFilterRules; - private List compactionMetricsRules; - private List compactionDimensionsRules; - private List compactionIOConfigRules; - private List compactionProjectionRules; - private List compactionGranularityRules; - private List compactionTuningConfigRules; - - public Builder filterRules(List compactionFilterRules) + private List compactionFilterRules; + private List compactionMetricsRules; + private List compactionDimensionsRules; + private List compactionIOConfigRules; + private List compactionProjectionRules; + private List compactionGranularityRules; + private List compactionTuningConfigRules; + + public Builder filterRules(List compactionFilterRules) { this.compactionFilterRules = compactionFilterRules; return this; } - public Builder metricsRules(List compactionMetricsRules) + public Builder metricsRules(List compactionMetricsRules) { this.compactionMetricsRules = compactionMetricsRules; return this; } - public Builder dimensionsRules(List compactionDimensionsRules) + public Builder dimensionsRules(List compactionDimensionsRules) { this.compactionDimensionsRules = compactionDimensionsRules; return this; } - public Builder ioConfigRules(List compactionIOConfigRules) + public Builder ioConfigRules(List compactionIOConfigRules) { this.compactionIOConfigRules = compactionIOConfigRules; return this; } - public Builder projectionRules(List compactionProjectionRules) + public Builder projectionRules(List compactionProjectionRules) { this.compactionProjectionRules = compactionProjectionRules; return this; } - public Builder granularityRules(List compactionGranularityRules) + public Builder granularityRules(List compactionGranularityRules) { this.compactionGranularityRules = compactionGranularityRules; return this; } - public Builder tuningConfigRules(List compactionTuningConfigRules) + public Builder tuningConfigRules(List compactionTuningConfigRules) { this.compactionTuningConfigRules = compactionTuningConfigRules; return this; } - public InlineCompactionRuleProvider build() + public InlineReindexingRuleProvider build() { - return new InlineCompactionRuleProvider( + return new InlineReindexingRuleProvider( compactionFilterRules, compactionMetricsRules, compactionDimensionsRules, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java index e3a891e91a7f..1351f73111bf 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionDimensionsRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java @@ -56,12 +56,12 @@ * } * } */ -public class CompactionDimensionsRule extends AbstractCompactionRule +public class ReindexingDimensionsRule extends AbstractReindexingRule { private final UserCompactionTaskDimensionsConfig dimensionsSpec; @JsonCreator - public CompactionDimensionsRule( + public ReindexingDimensionsRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index 64794619af9e..2983a7bfa4f9 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -56,13 +56,13 @@ * } * } */ -public class CompactionFilterRule extends AbstractCompactionRule +public class ReindexingFilterRule extends AbstractReindexingRule { private final DimFilter filter; @JsonCreator - public CompactionFilterRule( + public ReindexingFilterRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java index 5e7932ce0e06..40ca19065ddb 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionGranularityRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java @@ -54,13 +54,13 @@ * } * } */ -public class CompactionGranularityRule extends AbstractCompactionRule +public class ReindexingGranularityRule extends AbstractReindexingRule { private final UserCompactionTaskGranularityConfig granularityConfig; @JsonCreator - public CompactionGranularityRule( + public ReindexingGranularityRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java index 3e2cb08a20eb..5f1b1204a55b 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionIOConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java @@ -48,12 +48,12 @@ * } * } */ -public class CompactionIOConfigRule extends AbstractCompactionRule +public class ReindexingIOConfigRule extends AbstractReindexingRule { private final UserCompactionTaskIOConfig ioConfig; @JsonCreator - public CompactionIOConfigRule( + public ReindexingIOConfigRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java index 72d4ae0602f1..51d5d822c6b5 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionMetricsRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java @@ -54,12 +54,12 @@ * } * } */ -public class CompactionMetricsRule extends AbstractCompactionRule +public class ReindexingMetricsRule extends AbstractReindexingRule { private final AggregatorFactory[] metricsSpec; @JsonCreator - public CompactionMetricsRule( + public ReindexingMetricsRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java index 6f784a9cfa7b..9cb18899d7d7 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionProjectionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java @@ -59,12 +59,12 @@ * } * } */ -public class CompactionProjectionRule extends AbstractCompactionRule +public class ReindexingProjectionRule extends AbstractReindexingRule { private final List projections; @JsonCreator - public CompactionProjectionRule( + public ReindexingProjectionRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java similarity index 94% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java index bb6887e4738f..0407bf010ddf 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java @@ -26,12 +26,12 @@ import javax.annotation.Nullable; /** - * Defines a compaction configuration that applies to data based on age thresholds. + * Defines a reindexing configuration that applies to data based on age thresholds. *

- * Rules encapsulate specific aspects of compaction (granularity, filters, tuning, etc.) + * Rules encapsulate specific aspects of reindexing (granularity, filters, tuning, etc.) * and specify when they should apply via a period threshold. */ -public interface CompactionRule +public interface ReindexingRule { /** * Indicates how a rule applies to a given time interval based on the rule's period threshold. diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java similarity index 73% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 1d8f281b8743..023277ff3429 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -28,19 +28,19 @@ import java.util.List; /** - * Provides compaction rules for different aspects of compaction configuration. + * Provides compaction rules for different aspects of reindexing configuration. *

* This abstraction allows rules to be sourced from different locations: inline definitions, * database storage, external services, or dynamically generated based on metrics. Each method - * returns rules for a specific compaction aspect (granularity, filters, tuning, etc.), either + * returns rules for a specific reindexing aspect (granularity, filters, tuning, etc.), either * for all rules or filtered by interval applicability. */ @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") @JsonSubTypes(value = { - @JsonSubTypes.Type(name = InlineCompactionRuleProvider.TYPE, value = InlineCompactionRuleProvider.class), - @JsonSubTypes.Type(name = ComposingCompactionRuleProvider.TYPE, value = ComposingCompactionRuleProvider.class) + @JsonSubTypes.Type(name = InlineReindexingRuleProvider.TYPE, value = InlineReindexingRuleProvider.class), + @JsonSubTypes.Type(name = ComposingReindexingRuleProvider.TYPE, value = ComposingReindexingRuleProvider.class) }) -public interface CompactionRuleProvider +public interface ReindexingRuleProvider { /** * Returns the type identifier for this provider implementation. @@ -56,7 +56,7 @@ public interface CompactionRuleProvider * Returns true if this provider is ready to supply rules. *

* Providers that depend on external state (HTTP services, databases) should return false - * until they have successfully initialized and loaded their rules. Compaction supervisors + * until they have successfully initialized and loaded their rules. Reindexing supervisors * should check this before generating tasks to avoid creating tasks with incomplete rule sets. *

* The default implementation returns true, which is appropriate for providers that have @@ -78,112 +78,112 @@ default boolean isReady() List getCondensedAndSortedPeriods(DateTime referenceTime); /** - * Returns all compaction filter rules that apply to the given interval. + * Returns all reindexing filter rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionFilterRule} rules that apply to the given interval. + * @return The list of {@link ReindexingFilterRule} rules that apply to the given interval. */ - List getFilterRules(Interval interval, DateTime referenceTime); + List getFilterRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction filter rules. + * Returns ALL reindexing filter rules. */ - List getFilterRules(); + List getFilterRules(); /** - * Returns all compaction metrics rules that apply to the given interval. + * Returns all reindexing metrics rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionMetricsRule} rules that apply to the given interval. + * @return The list of {@link ReindexingMetricsRule} rules that apply to the given interval. */ - List getMetricsRules(Interval interval, DateTime referenceTime); + List getMetricsRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction metrics rules. + * Returns ALL reindexing metrics rules. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

*/ - List getMetricsRules(); + List getMetricsRules(); /** - * Returns all compaction dimensions rules that apply to the given interval. + * Returns all reindexing dimensions rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionDimensionsRule} rules that apply to the given interval. + * @return The list of {@link ReindexingDimensionsRule} rules that apply to the given interval. */ - List getDimensionsRules(Interval interval, DateTime referenceTime); + List getDimensionsRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction dimensions rules. + * Returns ALL reindexing dimensions rules. */ - List getDimensionsRules(); + List getDimensionsRules(); /** - * Returns all compaction IO config rules that apply to the given interval. + * Returns all reindexing IO config rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionIOConfigRule} rules that apply to the given interval. + * @return The list of {@link ReindexingIOConfigRule} rules that apply to the given interval. */ - List getIOConfigRules(Interval interval, DateTime referenceTime); + List getIOConfigRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction IO config rules. + * Returns ALL reindexing IO config rules. */ - List getIOConfigRules(); + List getIOConfigRules(); /** - * Returns all compaction projection rules that apply to the given interval. + * Returns all reindexing projection rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionProjectionRule} rules that apply to the given interval. + * @return The list of {@link ReindexingProjectionRule} rules that apply to the given interval. */ - List getProjectionRules(Interval interval, DateTime referenceTime); + List getProjectionRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction projection rules. + * Returns ALL reindexing projection rules. */ - List getProjectionRules(); + List getProjectionRules(); /** - * Returns all compaction granularity rules that apply to the given interval. + * Returns all reindexing granularity rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link CompactionGranularityRule} rules that apply to the given interval. + * @return The list of {@link ReindexingGranularityRule} rules that apply to the given interval. */ - List getGranularityRules(Interval interval, DateTime referenceTime); + List getGranularityRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction granularity rules. + * Returns ALL reindexing granularity rules. */ - List getGranularityRules(); + List getGranularityRules(); /** - * Returns all compaction tuning config rules that apply to the given interval. + * Returns all reindexing tuning config rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

@@ -191,10 +191,10 @@ default boolean isReady() * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. */ - List getTuningConfigRules(Interval interval, DateTime referenceTime); + List getTuningConfigRules(Interval interval, DateTime referenceTime); /** - * Returns ALL compaction tuning config rules. + * Returns ALL reindexing tuning config rules. */ - List getTuningConfigRules(); + List getTuningConfigRules(); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java similarity index 96% rename from server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java index bc1cfc4c242c..fe068272b137 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionTuningConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java @@ -57,12 +57,12 @@ * } * } */ -public class CompactionTuningConfigRule extends AbstractCompactionRule +public class ReindexingTuningConfigRule extends AbstractReindexingRule { private final UserCompactionTaskQueryTuningConfig tuningConfig; @JsonCreator - public CompactionTuningConfigRule( + public ReindexingTuningConfigRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java index 0c0a3c8d1537..446b8d3de974 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java @@ -58,6 +58,7 @@ import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; +import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -67,7 +68,6 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -911,7 +911,7 @@ public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); Assert.assertEquals(expectedFilter, result); @@ -925,7 +925,7 @@ public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() DimFilter filterC = new SelectorDimFilter("country", "FR", null); CompactionState state = createStateWithFilters("fp1", filterA, filterB, filterC); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); @@ -934,7 +934,7 @@ public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); Assert.assertNull(result); @@ -948,7 +948,7 @@ public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() DimFilter filterC = new SelectorDimFilter("country", "FR", null); CompactionState state = createStateWithoutFilters("fp1"); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); @@ -957,7 +957,7 @@ public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); OrDimFilter innerOr = (OrDimFilter) result.getField(); @@ -974,7 +974,7 @@ public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() DimFilter filterD = new SelectorDimFilter("country", "DE", null); CompactionState state = createStateWithFilters("fp1", filterA, filterB); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter( @@ -985,7 +985,7 @@ public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); OrDimFilter innerOr = (OrDimFilter) result.getField(); @@ -1005,7 +1005,9 @@ public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() CompactionState state1 = createStateWithFilters("fp1", filterA, filterB); CompactionState state2 = createStateWithFilters("fp2", filterA, filterC); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state1, "fp2", state2), DateTimes.nowUtc()); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter( @@ -1016,7 +1018,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); OrDimFilter innerOr = (OrDimFilter) result.getField(); @@ -1035,7 +1037,9 @@ public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() CompactionState state1 = createStateWithFilters("fp1", filterA); CompactionState state2 = createStateWithFilters("fp2", filterA); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state1, "fp2", state2), DateTimes.nowUtc()); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter( @@ -1046,7 +1050,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); OrDimFilter innerOr = (OrDimFilter) result.getField(); @@ -1072,7 +1076,7 @@ public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters( NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); Assert.assertEquals(expectedFilter, result); @@ -1086,7 +1090,7 @@ public void testComputeRequiredFilters_TransformSpecWithSingleFilter() DimFilter filterC = new SelectorDimFilter("country", "FR", null); CompactionState state = createStateWithSingleFilter("fp1", filterA); - compactionStateManager.persistCompactionState(TestDataSource.WIKI, Map.of("fp1", state), DateTimes.nowUtc()); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); syncCacheFromManager(); NotDimFilter expectedFilter = new NotDimFilter( @@ -1097,7 +1101,7 @@ public void testComputeRequiredFilters_TransformSpecWithSingleFilter() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); OrDimFilter innerOr = (OrDimFilter) result.getField(); @@ -1121,7 +1125,7 @@ public void testComputeRequiredFilters_SegmentsWithNoFingerprints() NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, - compactionStateCache + fingerprintMapper ); Assert.assertEquals(expectedFilter, result); @@ -1132,7 +1136,7 @@ public void testComputeRequiredFilters_SegmentsWithNoFingerprints() private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) { List segments = Arrays.stream(fingerprints) - .map(fp -> DataSegment.builder(WIKI_SEGMENT).compactionStateFingerprint(fp).build()) + .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) .collect(Collectors.toList()); return CompactionCandidate.from(segments, null); } @@ -1141,7 +1145,7 @@ private CompactionCandidate createCandidateWithNullFingerprints(int count) { List segments = new ArrayList<>(); for (int i = 0; i < count; i++) { - segments.add(DataSegment.builder(WIKI_SEGMENT).compactionStateFingerprint(null).build()); + segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); } return CompactionCandidate.from(segments, null); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java similarity index 62% rename from server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 5cd42b112ca5..e75024f13816 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingCompactionRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -33,7 +33,7 @@ import java.util.Collections; import java.util.List; -public class ComposingCompactionRuleProviderTest +public class ComposingReindexingRuleProviderTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); @@ -42,20 +42,20 @@ public void test_constructor_nullProviders_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ComposingCompactionRuleProvider(null) + () -> new ComposingReindexingRuleProvider(null) ); } @Test public void test_constructor_nullProviderInList_throwsNullPointerException() { - List providers = new ArrayList<>(); + List providers = new ArrayList<>(); providers.add(createEmptyInlineProvider()); providers.add(null); // Null provider NullPointerException exception = Assert.assertThrows( NullPointerException.class, - () -> new ComposingCompactionRuleProvider(providers) + () -> new ComposingReindexingRuleProvider(providers) ); Assert.assertTrue(exception.getMessage().contains("index 1")); @@ -64,7 +64,7 @@ public void test_constructor_nullProviderInList_throwsNullPointerException() @Test public void test_constructor_emptyProviderList_succeeds() { - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( Collections.emptyList() ); @@ -76,7 +76,7 @@ public void test_constructor_emptyProviderList_succeeds() @Test public void test_getType_returnsComposing() { - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(createEmptyInlineProvider()) ); @@ -86,10 +86,10 @@ public void test_getType_returnsComposing() @Test public void test_isReady_allProvidersReady_returnsTrue() { - CompactionRuleProvider provider1 = createEmptyInlineProvider(); // Always ready - CompactionRuleProvider provider2 = createEmptyInlineProvider(); // Always ready + ReindexingRuleProvider provider1 = createEmptyInlineProvider(); // Always ready + ReindexingRuleProvider provider2 = createEmptyInlineProvider(); // Always ready - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); @@ -99,10 +99,10 @@ public void test_isReady_allProvidersReady_returnsTrue() @Test public void test_isReady_someProvidersNotReady_returnsFalse() { - CompactionRuleProvider readyProvider = createEmptyInlineProvider(); - CompactionRuleProvider notReadyProvider = createNotReadyProvider(); + ReindexingRuleProvider readyProvider = createEmptyInlineProvider(); + ReindexingRuleProvider notReadyProvider = createNotReadyProvider(); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(readyProvider, notReadyProvider) ); @@ -112,7 +112,7 @@ public void test_isReady_someProvidersNotReady_returnsFalse() @Test public void test_isReady_emptyProviderList_returnsTrue() { - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( Collections.emptyList() ); @@ -122,17 +122,17 @@ public void test_isReady_emptyProviderList_returnsTrue() @Test public void test_getFilterRules_firstWins_returnsFirstNonEmpty() { - CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(30)); - CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(60)); + ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(30)); + ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(60)); - CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); + ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); - List result = composing.getFilterRules(); + List result = composing.getFilterRules(); Assert.assertEquals(1, result.size()); Assert.assertEquals("rule1", result.get(0).getId()); @@ -141,16 +141,16 @@ public void test_getFilterRules_firstWins_returnsFirstNonEmpty() @Test public void test_getFilterRules_firstProviderEmpty_returnsSecond() { - CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(60)); + ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(60)); - CompactionRuleProvider emptyProvider = createEmptyInlineProvider(); - CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); + ReindexingRuleProvider emptyProvider = createEmptyInlineProvider(); + ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(emptyProvider, provider2) ); - List result = composing.getFilterRules(); + List result = composing.getFilterRules(); Assert.assertEquals(1, result.size()); Assert.assertEquals("rule2", result.get(0).getId()); @@ -159,14 +159,14 @@ public void test_getFilterRules_firstProviderEmpty_returnsSecond() @Test public void test_getFilterRules_allProvidersEmpty_returnsEmpty() { - CompactionRuleProvider provider1 = createEmptyInlineProvider(); - CompactionRuleProvider provider2 = createEmptyInlineProvider(); + ReindexingRuleProvider provider1 = createEmptyInlineProvider(); + ReindexingRuleProvider provider2 = createEmptyInlineProvider(); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); - List result = composing.getFilterRules(); + List result = composing.getFilterRules(); Assert.assertTrue(result.isEmpty()); } @@ -174,17 +174,17 @@ public void test_getFilterRules_allProvidersEmpty_returnsEmpty() @Test public void test_getGranularityRules_firstWins_returnsFirstNonEmpty() { - CompactionGranularityRule rule1 = createGranularityRule("rule1", Period.days(7)); - CompactionGranularityRule rule2 = createGranularityRule("rule2", Period.days(30)); + ReindexingGranularityRule rule1 = createGranularityRule("rule1", Period.days(7)); + ReindexingGranularityRule rule2 = createGranularityRule("rule2", Period.days(30)); - CompactionRuleProvider provider1 = createInlineProviderWithGranularityRules(ImmutableList.of(rule1)); - CompactionRuleProvider provider2 = createInlineProviderWithGranularityRules(ImmutableList.of(rule2)); + ReindexingRuleProvider provider1 = createInlineProviderWithGranularityRules(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = createInlineProviderWithGranularityRules(ImmutableList.of(rule2)); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); - List result = composing.getGranularityRules(); + List result = composing.getGranularityRules(); Assert.assertEquals(1, result.size()); Assert.assertEquals("rule1", result.get(0).getId()); @@ -194,16 +194,16 @@ public void test_getGranularityRules_firstWins_returnsFirstNonEmpty() public void test_getFilterRulesWithInterval_firstWins_delegatesToFirstProvider() { Interval interval = new Interval("2025-11-01T00:00:00Z/2025-11-15T00:00:00Z"); - CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(30)); + ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(30)); - CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - CompactionRuleProvider provider2 = createEmptyInlineProvider(); + ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = createEmptyInlineProvider(); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); - List result = composing.getFilterRules(interval, REFERENCE_TIME); + List result = composing.getFilterRules(interval, REFERENCE_TIME); Assert.assertEquals(1, result.size()); Assert.assertEquals("rule1", result.get(0).getId()); @@ -212,14 +212,14 @@ public void test_getFilterRulesWithInterval_firstWins_delegatesToFirstProvider() @Test public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() { - CompactionFilterRule rule1 = createFilterRule("rule1", Period.days(7)); - CompactionFilterRule rule2 = createFilterRule("rule2", Period.days(30)); - CompactionFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period + ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(7)); + ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(30)); + ReindexingFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period - CompactionRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - CompactionRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2, rule3)); + ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2, rule3)); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); @@ -234,14 +234,14 @@ public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() @Test public void test_singleProvider_delegatesDirectly() { - CompactionFilterRule rule = createFilterRule("rule1", Period.days(30)); - CompactionRuleProvider provider = createInlineProviderWithFilterRules(ImmutableList.of(rule)); + ReindexingFilterRule rule = createFilterRule("rule1", Period.days(30)); + ReindexingRuleProvider provider = createInlineProviderWithFilterRules(ImmutableList.of(rule)); - ComposingCompactionRuleProvider composing = new ComposingCompactionRuleProvider( + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider) ); - List result = composing.getFilterRules(); + List result = composing.getFilterRules(); Assert.assertEquals(1, result.size()); Assert.assertEquals("rule1", result.get(0).getId()); @@ -249,9 +249,9 @@ public void test_singleProvider_delegatesDirectly() // ========== Helper Methods ========== - private CompactionRuleProvider createEmptyInlineProvider() + private ReindexingRuleProvider createEmptyInlineProvider() { - return new InlineCompactionRuleProvider( + return new InlineReindexingRuleProvider( Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), @@ -262,9 +262,9 @@ private CompactionRuleProvider createEmptyInlineProvider() ); } - private CompactionRuleProvider createInlineProviderWithFilterRules(List rules) + private ReindexingRuleProvider createInlineProviderWithFilterRules(List rules) { - return new InlineCompactionRuleProvider( + return new InlineReindexingRuleProvider( rules, Collections.emptyList(), Collections.emptyList(), @@ -275,9 +275,9 @@ private CompactionRuleProvider createInlineProviderWithFilterRules(List rules) + private ReindexingRuleProvider createInlineProviderWithGranularityRules(List rules) { - return new InlineCompactionRuleProvider( + return new InlineReindexingRuleProvider( Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), @@ -288,9 +288,9 @@ private CompactionRuleProvider createInlineProviderWithGranularityRules(List getCondensedAndSortedPeriods(DateTime referenceTime) } @Override - public List getFilterRules(Interval interval, DateTime referenceTime) + public List getFilterRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getFilterRules() + public List getFilterRules() { return Collections.emptyList(); } @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) + public List getMetricsRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getMetricsRules() + public List getMetricsRules() { return Collections.emptyList(); } @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) + public List getDimensionsRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getDimensionsRules() + public List getDimensionsRules() { return Collections.emptyList(); } @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) + public List getIOConfigRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getIOConfigRules() + public List getIOConfigRules() { return Collections.emptyList(); } @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) + public List getProjectionRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getProjectionRules() + public List getProjectionRules() { return Collections.emptyList(); } @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) + public List getGranularityRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getGranularityRules() + public List getGranularityRules() { return Collections.emptyList(); } @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) + public List getTuningConfigRules(Interval interval, DateTime referenceTime) { return Collections.emptyList(); } @Override - public List getTuningConfigRules() + public List getTuningConfigRules() { return Collections.emptyList(); } }; } - private CompactionFilterRule createFilterRule(String id, Period period) + private ReindexingFilterRule createFilterRule(String id, Period period) { - return new CompactionFilterRule( + return new ReindexingFilterRule( id, "Test rule", period, @@ -406,9 +406,9 @@ private CompactionFilterRule createFilterRule(String id, Period period) ); } - private CompactionGranularityRule createGranularityRule(String id, Period period) + private ReindexingGranularityRule createGranularityRule(String id, Period period) { - return new CompactionGranularityRule( + return new ReindexingGranularityRule( id, "Test granularity rule", period, diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java similarity index 80% rename from server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java rename to server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 53b8111089f6..216d30d1dd07 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineCompactionRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -31,7 +31,7 @@ import java.util.Collections; import java.util.List; -public class InlineCompactionRuleProviderTest +public class InlineReindexingRuleProviderTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); @@ -55,17 +55,17 @@ public class InlineCompactionRuleProviderTest @Test public void test_getFilterRules_noRulesMatch_returnsEmpty() { - CompactionFilterRule rule30d = new CompactionFilterRule( + ReindexingFilterRule rule30d = new ReindexingFilterRule( "filter-30d", null, Period.days(30), new SelectorDimFilter("dim", "val", null) ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d)).build(); // Interval is only 5 days old, rule requires 30 days - List result = provider.getFilterRules(INTERVAL_5_DAYS_OLD, REFERENCE_TIME); + List result = provider.getFilterRules(INTERVAL_5_DAYS_OLD, REFERENCE_TIME); Assert.assertTrue("Should return empty when no rules match", result.isEmpty()); } @@ -73,17 +73,17 @@ public void test_getFilterRules_noRulesMatch_returnsEmpty() @Test public void test_getFilterRules_oneRuleMatchesFull_returnsThatRule() { - CompactionFilterRule rule30d = new CompactionFilterRule( + ReindexingFilterRule rule30d = new ReindexingFilterRule( "filter-30d", null, Period.days(30), new SelectorDimFilter("dim", "val", null) ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d)).build(); // Interval is 50 days old, rule requires 30 days - FULL match - List result = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + List result = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals(1, result.size()); Assert.assertEquals("filter-30d", result.get(0).getId()); @@ -93,21 +93,21 @@ public void test_getFilterRules_oneRuleMatchesFull_returnsThatRule() public void test_getFilterRules_multipleAdditiveRulesMatchFull_returnsAll() { // Filter rules are additive - should return all matching rules - CompactionFilterRule rule30d = new CompactionFilterRule( + ReindexingFilterRule rule30d = new ReindexingFilterRule( "filter-30d", null, Period.days(30), new SelectorDimFilter("dim1", "val1", null) ); - CompactionFilterRule rule60d = new CompactionFilterRule( + ReindexingFilterRule rule60d = new ReindexingFilterRule( "filter-60d", null, Period.days(60), new SelectorDimFilter("dim2", "val2", null) ); - CompactionFilterRule rule90d = new CompactionFilterRule( + ReindexingFilterRule rule90d = new ReindexingFilterRule( "filter-90d", null, Period.days(90), @@ -115,10 +115,10 @@ public void test_getFilterRules_multipleAdditiveRulesMatchFull_returnsAll() ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(rule30d, rule60d, rule90d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d, rule60d, rule90d)).build(); // Interval is 100 days old - all three rules match - List result = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + List result = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals("Should return all matching additive rules", 3, result.size()); Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-30d"))); @@ -130,32 +130,32 @@ public void test_getFilterRules_multipleAdditiveRulesMatchFull_returnsAll() public void test_getGranularityRules_multipleNonAdditiveRulesMatchFull_returnsOldestThreshold() { // Granularity rules are NOT additive - should return only the one with oldest threshold - CompactionGranularityRule rule30d = new CompactionGranularityRule( + ReindexingGranularityRule rule30d = new ReindexingGranularityRule( "gran-30d", null, Period.days(30), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); - CompactionGranularityRule rule60d = new CompactionGranularityRule( + ReindexingGranularityRule rule60d = new ReindexingGranularityRule( "gran-60d", null, Period.days(60), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) ); - CompactionGranularityRule rule90d = new CompactionGranularityRule( + ReindexingGranularityRule rule90d = new ReindexingGranularityRule( "gran-90d", null, Period.days(90), new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule30d, rule60d, rule90d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule30d, rule60d, rule90d)).build(); // Interval is 100 days old - all three rules match FULL // Should return rule90d because it has the oldest threshold (now - 90d) - List result = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + List result = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals("Should return only one non-additive rule", 1, result.size()); Assert.assertEquals("gran-90d", result.get(0).getId()); @@ -165,18 +165,18 @@ public void test_getGranularityRules_multipleNonAdditiveRulesMatchFull_returnsOl @Test public void test_getGranularityRules_partialMatchNotReturned() { - CompactionGranularityRule rule30d = new CompactionGranularityRule( + ReindexingGranularityRule rule30d = new ReindexingGranularityRule( "gran-30d", null, Period.days(30), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule30d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule30d)).build(); // Interval is 20 days old, but rule requires 30 days // The interval likely has PARTIAL or NONE match, not FULL - List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); Assert.assertTrue("Should not return rules with PARTIAL match", result.isEmpty()); } @@ -184,20 +184,20 @@ public void test_getGranularityRules_partialMatchNotReturned() @Test public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() { - CompactionFilterRule filter30d = new CompactionFilterRule( + ReindexingFilterRule filter30d = new ReindexingFilterRule( "f1", null, Period.days(30), new SelectorDimFilter("d", "v", null) ); - CompactionFilterRule filter60d = new CompactionFilterRule( + ReindexingFilterRule filter60d = new ReindexingFilterRule( "f2", null, Period.days(60), new SelectorDimFilter("d", "v", null) ); - CompactionGranularityRule gran30d = new CompactionGranularityRule( + ReindexingGranularityRule gran30d = new ReindexingGranularityRule( "g1", null, Period.days(30), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); - CompactionGranularityRule gran90d = new CompactionGranularityRule( + ReindexingGranularityRule gran90d = new ReindexingGranularityRule( "g2", null, Period.days(90), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(List.of(filter30d, filter60d)).granularityRules(List.of(gran30d, gran90d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(filter30d, filter60d)).granularityRules(List.of(gran30d, gran90d)).build(); List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); @@ -212,7 +212,7 @@ public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() @Test public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() { - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().filterRules(Collections.emptyList()).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(Collections.emptyList()).build(); List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); @@ -223,17 +223,17 @@ public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() public void test_getProjectionRules_multipleAdditiveRulesMatchFull_returnsAll() { // Projection rules are additive - CompactionProjectionRule proj30d = new CompactionProjectionRule( + ReindexingProjectionRule proj30d = new ReindexingProjectionRule( "proj-30d", null, Period.days(30), Collections.emptyList() ); - CompactionProjectionRule proj60d = new CompactionProjectionRule( + ReindexingProjectionRule proj60d = new ReindexingProjectionRule( "proj-60d", null, Period.days(60), Collections.emptyList() ); - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().projectionRules(List.of(proj30d, proj60d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().projectionRules(List.of(proj30d, proj60d)).build(); // Interval is 100 days old - both rules match - List result = provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + List result = provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals("Should return all matching additive projection rules", 2, result.size()); } @@ -242,35 +242,35 @@ public void test_getProjectionRules_multipleAdditiveRulesMatchFull_returnsAll() public void test_getApplicableRules_mixOfFullPartialNone_onlyReturnsFull() { // Create rules that will have different AppliesToMode results - CompactionGranularityRule rule10d = new CompactionGranularityRule( + ReindexingGranularityRule rule10d = new ReindexingGranularityRule( "gran-10d", null, Period.days(10), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); // Will match FULL for 20-day-old interval - CompactionGranularityRule rule25d = new CompactionGranularityRule( + ReindexingGranularityRule rule25d = new ReindexingGranularityRule( "gran-25d", null, Period.days(25), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) ); // Will match FULL for 20-day-old interval - CompactionGranularityRule rule50d = new CompactionGranularityRule( + ReindexingGranularityRule rule50d = new ReindexingGranularityRule( "gran-50d", null, Period.days(50), new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) ); // Will be NONE for 20-day-old interval - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().granularityRules(List.of(rule10d, rule25d, rule50d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule10d, rule25d, rule50d)).build(); // Interval is 20 days old (ends at 2025-11-21, reference is 2025-12-19) // rule10d: threshold = now - 10d = 2025-12-09, interval ends 2025-11-21 < 2025-12-09 -> FULL // rule25d: threshold = now - 25d = 2025-11-24, interval ends 2025-11-21 < 2025-11-24 -> FULL // rule50d: threshold = now - 50d = 2025-10-30, interval ends 2025-11-21 > 2025-10-30 -> NONE // When multiple rules match, select the one with oldest threshold (smallest millis) = rule25d - List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); // Should return rule25d (has oldest threshold among FULL matches) Assert.assertEquals(1, result.size()); @@ -280,7 +280,7 @@ public void test_getApplicableRules_mixOfFullPartialNone_onlyReturnsFull() @Test public void test_constructor_nullListsDefaultToEmpty() { - InlineCompactionRuleProvider provider = new InlineCompactionRuleProvider( + InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider( null, null, null, @@ -301,7 +301,7 @@ public void test_constructor_nullListsDefaultToEmpty() @Test public void test_getType_returnsInline() { - InlineCompactionRuleProvider provider = InlineCompactionRuleProvider.builder().build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); Assert.assertEquals("inline", provider.getType()); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java similarity index 83% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java index 6126b6adf25f..d2f365838283 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionDimensionsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java @@ -26,12 +26,12 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionDimensionsRuleTest +public class ReindexingDimensionsRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_14_DAYS = Period.days(14); - private final CompactionDimensionsRule rule = new CompactionDimensionsRule( + private final ReindexingDimensionsRule rule = new ReindexingDimensionsRule( "test-dimensions-rule", "Custom dimensions config", PERIOD_14_DAYS, @@ -52,9 +52,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-12-03, which is fully before threshold Interval interval = new Interval("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -64,9 +64,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -76,9 +76,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -88,9 +88,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -125,7 +125,7 @@ public void test_constructor_nullId_throwsNullPointerException() UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); Assert.assertThrows( NullPointerException.class, - () -> new CompactionDimensionsRule(null, "description", PERIOD_14_DAYS, config) + () -> new ReindexingDimensionsRule(null, "description", PERIOD_14_DAYS, config) ); } @@ -135,7 +135,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); Assert.assertThrows( NullPointerException.class, - () -> new CompactionDimensionsRule("test-id", "description", null, config) + () -> new ReindexingDimensionsRule("test-id", "description", null, config) ); } @@ -146,7 +146,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionDimensionsRule("test-id", "description", zeroPeriod, config) + () -> new ReindexingDimensionsRule("test-id", "description", zeroPeriod, config) ); } @@ -157,7 +157,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-14); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionDimensionsRule("test-id", "description", negativePeriod, config) + () -> new ReindexingDimensionsRule("test-id", "description", negativePeriod, config) ); } @@ -166,7 +166,7 @@ public void test_constructor_nullDimensionsSpec_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionDimensionsRule("test-id", "description", PERIOD_14_DAYS, null) + () -> new ReindexingDimensionsRule("test-id", "description", PERIOD_14_DAYS, null) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java similarity index 84% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index 6a7e1bc571fa..5ffd19a45451 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -27,13 +27,13 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionFilterRuleTest +public class ReindexingFilterRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); - private final CompactionFilterRule rule = new CompactionFilterRule( + private final ReindexingFilterRule rule = new ReindexingFilterRule( "test-filter-rule", "Remove robot traffic", PERIOD_30_DAYS, @@ -54,9 +54,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-11-15, which is fully before threshold Interval interval = new Interval("2025-11-14T00:00:00Z/2025-11-15T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -66,9 +66,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-11-18T12:00:00Z/2025-11-19T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -78,9 +78,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-11-18T00:00:00Z/2025-11-20T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -90,9 +90,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -127,7 +127,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionFilterRule(null, "description", PERIOD_30_DAYS, testFilter) + () -> new ReindexingFilterRule(null, "description", PERIOD_30_DAYS, testFilter) ); } @@ -136,7 +136,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionFilterRule("test-id", "description", null, testFilter) + () -> new ReindexingFilterRule("test-id", "description", null, testFilter) ); } @@ -146,7 +146,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionFilterRule("test-id", "description", zeroPeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter) ); } @@ -156,7 +156,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-30); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionFilterRule("test-id", "description", negativePeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter) ); } @@ -165,7 +165,7 @@ public void test_constructor_nullFilter_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionFilterRule("test-id", "description", PERIOD_30_DAYS, null) + () -> new ReindexingFilterRule("test-id", "description", PERIOD_30_DAYS, null) ); } @@ -176,7 +176,7 @@ public void test_constructor_periodWithMonths_succeeds() { // P6M should work - months are valid even though they're variable length Period period = Period.months(6); - CompactionFilterRule rule = new CompactionFilterRule( + ReindexingFilterRule rule = new ReindexingFilterRule( "test-id", "6 month rule", period, @@ -191,7 +191,7 @@ public void test_constructor_periodWithYears_succeeds() { // P1Y should work - years are valid even though they're variable length Period period = Period.years(1); - CompactionFilterRule rule = new CompactionFilterRule( + ReindexingFilterRule rule = new ReindexingFilterRule( "test-id", "1 year rule", period, @@ -206,7 +206,7 @@ public void test_constructor_periodWithMixedMonthsAndDays_succeeds() { // P6M15D should work - mixed months and days Period period = Period.months(6).plusDays(15); - CompactionFilterRule rule = new CompactionFilterRule( + ReindexingFilterRule rule = new ReindexingFilterRule( "test-id", "6 months 15 days rule", period, @@ -221,7 +221,7 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds() { // P1Y3M10D should work - complex period with years, months, and days Period period = Period.years(1).plusMonths(3).plusDays(10); - CompactionFilterRule rule = new CompactionFilterRule( + ReindexingFilterRule rule = new ReindexingFilterRule( "test-id", "1 year 3 months 10 days rule", period, @@ -238,7 +238,7 @@ public void test_constructor_zeroMonthsPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.months(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionFilterRule("test-id", "description", zeroPeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter) ); } @@ -249,7 +249,7 @@ public void test_constructor_negativeMonthsPeriod_throwsIllegalArgumentException Period negativePeriod = Period.months(-6); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionFilterRule("test-id", "description", negativePeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter) ); } @@ -262,7 +262,7 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() // Expected threshold: 2025-06-19T12:00:00Z (6 months before reference) Period sixMonths = Period.months(6); - CompactionFilterRule monthRule = new CompactionFilterRule( + ReindexingFilterRule monthRule = new ReindexingFilterRule( "test-month-rule", "6 months rule", sixMonths, @@ -272,21 +272,21 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() // Interval ending before 6-month threshold - should be FULL Interval beforeThreshold = new Interval("2025-06-01T00:00:00Z/2025-06-15T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.FULL, + ReindexingRule.AppliesToMode.FULL, monthRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 6-month threshold - should be PARTIAL Interval spanningThreshold = new Interval("2025-06-15T00:00:00Z/2025-07-15T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.PARTIAL, + ReindexingRule.AppliesToMode.PARTIAL, monthRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 6-month threshold - should be NONE Interval afterThreshold = new Interval("2025-07-01T00:00:00Z/2025-07-15T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.NONE, + ReindexingRule.AppliesToMode.NONE, monthRule.appliesTo(afterThreshold, REFERENCE_TIME) ); } @@ -300,7 +300,7 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() // Expected threshold: 2024-12-19T12:00:00Z (1 year before reference) Period oneYear = Period.years(1); - CompactionFilterRule yearRule = new CompactionFilterRule( + ReindexingFilterRule yearRule = new ReindexingFilterRule( "test-year-rule", "1 year rule", oneYear, @@ -310,21 +310,21 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() // Interval ending before 1-year threshold - should be FULL Interval beforeThreshold = new Interval("2024-11-01T00:00:00Z/2024-12-01T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.FULL, + ReindexingRule.AppliesToMode.FULL, yearRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 1-year threshold - should be PARTIAL Interval spanningThreshold = new Interval("2024-12-01T00:00:00Z/2025-01-01T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.PARTIAL, + ReindexingRule.AppliesToMode.PARTIAL, yearRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 1-year threshold - should be NONE Interval afterThreshold = new Interval("2025-01-01T00:00:00Z/2025-02-01T00:00:00Z"); Assert.assertEquals( - CompactionRule.AppliesToMode.NONE, + ReindexingRule.AppliesToMode.NONE, yearRule.appliesTo(afterThreshold, REFERENCE_TIME) ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java similarity index 84% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java index f11b5dc3777f..6c6f6d3a75a0 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java @@ -27,12 +27,12 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionGranularityRuleTest +public class ReindexingGranularityRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_7_DAYS = Period.days(7); - private final CompactionGranularityRule rule = new CompactionGranularityRule( + private final ReindexingGranularityRule rule = new ReindexingGranularityRule( "test-rule", "Test granularity rule", PERIOD_7_DAYS, @@ -53,9 +53,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-12-10, which is fully before threshold Interval interval = new Interval("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -65,9 +65,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -77,9 +77,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -89,9 +89,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -131,7 +131,7 @@ public void test_constructor_nullId_throwsNullPointerException() ); Assert.assertThrows( NullPointerException.class, - () -> new CompactionGranularityRule(null, "description", PERIOD_7_DAYS, config) + () -> new ReindexingGranularityRule(null, "description", PERIOD_7_DAYS, config) ); } @@ -145,7 +145,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() ); Assert.assertThrows( NullPointerException.class, - () -> new CompactionGranularityRule("test-id", "description", null, config) + () -> new ReindexingGranularityRule("test-id", "description", null, config) ); } @@ -160,7 +160,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionGranularityRule("test-id", "description", zeroPeriod, config) + () -> new ReindexingGranularityRule("test-id", "description", zeroPeriod, config) ); } @@ -175,7 +175,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-7); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionGranularityRule("test-id", "description", negativePeriod, config) + () -> new ReindexingGranularityRule("test-id", "description", negativePeriod, config) ); } @@ -184,7 +184,7 @@ public void test_constructor_nullGranularityConfig_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionGranularityRule("test-id", "description", PERIOD_7_DAYS, null) + () -> new ReindexingGranularityRule("test-id", "description", PERIOD_7_DAYS, null) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java similarity index 83% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index 5414eae9f1bd..a690a2b5c4af 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -26,12 +26,12 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionIOConfigRuleTest +public class ReindexingIOConfigRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_60_DAYS = Period.days(60); - private final CompactionIOConfigRule rule = new CompactionIOConfigRule( + private final ReindexingIOConfigRule rule = new ReindexingIOConfigRule( "test-ioconfig-rule", "Custom IO config", PERIOD_60_DAYS, @@ -52,9 +52,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-10-15, which is fully before threshold Interval interval = new Interval("2025-10-14T00:00:00Z/2025-10-15T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -64,9 +64,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-10-19T12:00:00Z/2025-10-20T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -76,9 +76,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-10-19T00:00:00Z/2025-10-21T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -88,9 +88,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -125,7 +125,7 @@ public void test_constructor_nullId_throwsNullPointerException() UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); Assert.assertThrows( NullPointerException.class, - () -> new CompactionIOConfigRule(null, "description", PERIOD_60_DAYS, config) + () -> new ReindexingIOConfigRule(null, "description", PERIOD_60_DAYS, config) ); } @@ -135,7 +135,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); Assert.assertThrows( NullPointerException.class, - () -> new CompactionIOConfigRule("test-id", "description", null, config) + () -> new ReindexingIOConfigRule("test-id", "description", null, config) ); } @@ -146,7 +146,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionIOConfigRule("test-id", "description", zeroPeriod, config) + () -> new ReindexingIOConfigRule("test-id", "description", zeroPeriod, config) ); } @@ -157,7 +157,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-60); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionIOConfigRule("test-id", "description", negativePeriod, config) + () -> new ReindexingIOConfigRule("test-id", "description", negativePeriod, config) ); } @@ -166,7 +166,7 @@ public void test_constructor_nullIOConfig_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionIOConfigRule("test-id", "description", PERIOD_60_DAYS, null) + () -> new ReindexingIOConfigRule("test-id", "description", PERIOD_60_DAYS, null) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java similarity index 84% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java index cb188560debb..ee20986d7903 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionMetricsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java @@ -28,7 +28,7 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionMetricsRuleTest +public class ReindexingMetricsRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_90_DAYS = Period.days(90); @@ -38,7 +38,7 @@ public class CompactionMetricsRuleTest new LongSumAggregatorFactory("total_value", "value") }; - private final CompactionMetricsRule rule = new CompactionMetricsRule( + private final ReindexingMetricsRule rule = new ReindexingMetricsRule( "test-metrics-rule", "Aggregate metrics for old data", PERIOD_90_DAYS, @@ -59,9 +59,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-09-15, which is fully before threshold Interval interval = new Interval("2025-09-14T00:00:00Z/2025-09-15T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -71,9 +71,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-09-19T12:00:00Z/2025-09-20T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -83,9 +83,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-09-19T00:00:00Z/2025-09-21T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -95,9 +95,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-01T00:00:00Z/2025-12-02T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -134,7 +134,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionMetricsRule(null, "description", PERIOD_90_DAYS, testMetrics) + () -> new ReindexingMetricsRule(null, "description", PERIOD_90_DAYS, testMetrics) ); } @@ -143,7 +143,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionMetricsRule("test-id", "description", null, testMetrics) + () -> new ReindexingMetricsRule("test-id", "description", null, testMetrics) ); } @@ -153,7 +153,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionMetricsRule("test-id", "description", zeroPeriod, testMetrics) + () -> new ReindexingMetricsRule("test-id", "description", zeroPeriod, testMetrics) ); } @@ -163,7 +163,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-90); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionMetricsRule("test-id", "description", negativePeriod, testMetrics) + () -> new ReindexingMetricsRule("test-id", "description", negativePeriod, testMetrics) ); } @@ -172,7 +172,7 @@ public void test_constructor_nullMetricsSpec_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionMetricsRule("test-id", "description", PERIOD_90_DAYS, null) + () -> new ReindexingMetricsRule("test-id", "description", PERIOD_90_DAYS, null) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java similarity index 82% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java index d2761eb8a3be..6a37ddb78c8e 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionProjectionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java @@ -27,12 +27,12 @@ import java.util.Collections; -public class CompactionProjectionRuleTest +public class ReindexingProjectionRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_45_DAYS = Period.days(45); - private final CompactionProjectionRule rule = new CompactionProjectionRule( + private final ReindexingProjectionRule rule = new ReindexingProjectionRule( "test-projection-rule", "Add aggregate projections", PERIOD_45_DAYS, @@ -53,9 +53,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-11-01, which is fully before threshold Interval interval = new Interval("2025-10-31T00:00:00Z/2025-11-01T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -65,9 +65,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-11-03T12:00:00Z/2025-11-04T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -77,9 +77,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-11-03T00:00:00Z/2025-11-05T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -89,9 +89,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -124,7 +124,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionProjectionRule(null, "description", PERIOD_45_DAYS, Collections.emptyList()) + () -> new ReindexingProjectionRule(null, "description", PERIOD_45_DAYS, Collections.emptyList()) ); } @@ -133,7 +133,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionProjectionRule("test-id", "description", null, Collections.emptyList()) + () -> new ReindexingProjectionRule("test-id", "description", null, Collections.emptyList()) ); } @@ -143,7 +143,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionProjectionRule("test-id", "description", zeroPeriod, Collections.emptyList()) + () -> new ReindexingProjectionRule("test-id", "description", zeroPeriod, Collections.emptyList()) ); } @@ -153,7 +153,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-45); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionProjectionRule("test-id", "description", negativePeriod, Collections.emptyList()) + () -> new ReindexingProjectionRule("test-id", "description", negativePeriod, Collections.emptyList()) ); } @@ -162,7 +162,7 @@ public void test_constructor_nullProjections_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionProjectionRule("test-id", "description", PERIOD_45_DAYS, null) + () -> new ReindexingProjectionRule("test-id", "description", PERIOD_45_DAYS, null) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java similarity index 84% rename from server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index a86718f66a10..1630a9416867 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -27,12 +27,12 @@ import org.junit.Assert; import org.junit.Test; -public class CompactionTuningConfigRuleTest +public class ReindexingTuningConfigRuleTest { private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); private static final Period PERIOD_21_DAYS = Period.days(21); - private final CompactionTuningConfigRule rule = new CompactionTuningConfigRule( + private final ReindexingTuningConfigRule rule = new ReindexingTuningConfigRule( "test-tuning-rule", "Custom tuning config", PERIOD_21_DAYS, @@ -54,9 +54,9 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() // Interval ends at 2025-11-25, which is fully before threshold Interval interval = new Interval("2025-11-24T00:00:00Z/2025-11-25T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -66,9 +66,9 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() // Interval ends exactly at threshold - should be FULL (boundary case) Interval interval = new Interval("2025-11-27T12:00:00Z/2025-11-28T12:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.FULL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -78,9 +78,9 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() // Interval starts before threshold and ends after - PARTIAL Interval interval = new Interval("2025-11-27T00:00:00Z/2025-11-29T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.PARTIAL, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -90,9 +90,9 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() // Interval starts after threshold - NONE Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); - CompactionRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(CompactionRule.AppliesToMode.NONE, result); + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -127,7 +127,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionTuningConfigRule(null, "description", PERIOD_21_DAYS, createTestTuningConfig()) + () -> new ReindexingTuningConfigRule(null, "description", PERIOD_21_DAYS, createTestTuningConfig()) ); } @@ -136,7 +136,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionTuningConfigRule("test-id", "description", null, createTestTuningConfig()) + () -> new ReindexingTuningConfigRule("test-id", "description", null, createTestTuningConfig()) ); } @@ -146,7 +146,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionTuningConfigRule("test-id", "description", zeroPeriod, createTestTuningConfig()) + () -> new ReindexingTuningConfigRule("test-id", "description", zeroPeriod, createTestTuningConfig()) ); } @@ -156,7 +156,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-21); Assert.assertThrows( IllegalArgumentException.class, - () -> new CompactionTuningConfigRule("test-id", "description", negativePeriod, createTestTuningConfig()) + () -> new ReindexingTuningConfigRule("test-id", "description", negativePeriod, createTestTuningConfig()) ); } @@ -165,7 +165,7 @@ public void test_constructor_nullTuningConfig_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new CompactionTuningConfigRule("test-id", "description", PERIOD_21_DAYS, null) + () -> new ReindexingTuningConfigRule("test-id", "description", PERIOD_21_DAYS, null) ); } From 71830fccb9d6fdc8c8e4f04e2358b2dbd1700841 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 15 Jan 2026 16:59:26 -0600 Subject: [PATCH 03/90] add support for more config knobs in the cascading reindex spec --- .../compact/CompactionSupervisorTest.java | 9 +- .../compact/CascadingReindexingTemplate.java | 91 ++++++++++++------- 2 files changed, 66 insertions(+), 34 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 1abe8342c205..649ef743116b 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -358,7 +358,14 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac .tuningConfigRules(List.of(tuningConfigRule)) .build(); - CascadingReindexingTemplate cascadingReindexingTemplate = new CascadingReindexingTemplate(dataSource, ruleProvider); + CascadingReindexingTemplate cascadingReindexingTemplate = new CascadingReindexingTemplate( + dataSource, + null, + null, + ruleProvider, + compactionEngine, + null + ); runCompactionWithSpec(cascadingReindexingTemplate); waitForAllCompactionTasksToFinish(); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index a01e5114ab61..b4aaf4e2e1e8 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -85,15 +85,33 @@ public class CascadingReindexingTemplate implements CompactionJobTemplate, DataS private final String dataSource; private final ReindexingRuleProvider ruleProvider; + @Nullable + private final Map taskContext; + @Nullable + private final CompactionEngine engine; + private final int taskPriority; + private final long inputSegmentSizeBytes; @JsonCreator public CascadingReindexingTemplate( @JsonProperty("dataSource") String dataSource, - @JsonProperty("ruleProvider") ReindexingRuleProvider ruleProvider + @JsonProperty("taskPriority") @Nullable Integer taskPriority, + @JsonProperty("inputSegmentSizeBytes") @Nullable Long inputSegmentSizeBytes, + @JsonProperty("ruleProvider") ReindexingRuleProvider ruleProvider, + @JsonProperty("engine") @Nullable CompactionEngine engine, + @JsonProperty("taskContext") @Nullable Map taskContext ) { this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); this.ruleProvider = Objects.requireNonNull(ruleProvider, "'ruleProvider' cannot be null"); + this.engine = engine; + this.taskContext = taskContext; + this.taskPriority = taskPriority == null + ? DEFAULT_COMPACTION_TASK_PRIORITY + : taskPriority; + this.inputSegmentSizeBytes = inputSegmentSizeBytes == null + ? DEFAULT_INPUT_SEGMENT_SIZE_BYTES + : inputSegmentSizeBytes; } @Override @@ -103,6 +121,41 @@ public String getDataSource() return dataSource; } + @JsonProperty + @Nullable + @Override + public Map getTaskContext() + { + return taskContext; + } + + @JsonProperty + @Nullable + @Override + public CompactionEngine getEngine() + { + return engine; + } + + @JsonProperty + @Override + public int getTaskPriority() + { + return taskPriority; + } + + @Override + public long getInputSegmentSizeBytes() + { + return inputSegmentSizeBytes; + } + + @JsonProperty + private ReindexingRuleProvider getRuleProvider() + { + return ruleProvider; + } + /** * Creates a config finalizer that optimizes filter rules for cascading reindexing. * When a candidate segment has already been reindexed with a subset of filter rules, @@ -165,6 +218,10 @@ public List createCompactionJobs( for (Interval reindexingInterval : intervals) { InlineSchemaDataSourceCompactionConfig.Builder builder = InlineSchemaDataSourceCompactionConfig.builder() .forDataSource(dataSource) + .withTaskPriority(taskPriority) + .withInputSegmentSizeBytes(inputSegmentSizeBytes) + .withEngine(engine) + .withTaskContext(taskContext) .withSkipOffsetFromLatest(Period.ZERO); // Apply all applicable reindexing rules to the builder @@ -372,25 +429,6 @@ public String getType() // Legacy fields from DataSourceCompactionConfig that are not used by this template - @Nullable - @Override - public CompactionEngine getEngine() - { - return null; - } - - @Override - public int getTaskPriority() - { - return 0; - } - - @Override - public long getInputSegmentSizeBytes() - { - return 0; - } - @Nullable @Override public Integer getMaxRowsPerSegment() @@ -418,13 +456,6 @@ public UserCompactionTaskIOConfig getIoConfig() return null; } - @Nullable - @Override - public Map getTaskContext() - { - return Map.of(); - } - @Nullable @Override public Granularity getSegmentGranularity() @@ -466,10 +497,4 @@ public AggregatorFactory[] getMetricsSpec() { return new AggregatorFactory[0]; } - - @JsonProperty - private ReindexingRuleProvider getRuleProvider() - { - return ruleProvider; - } } From 1940267a8495ed2db024a9207d127800e1160717 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 15 Jan 2026 17:44:14 -0600 Subject: [PATCH 04/90] add some testing --- .../compact/CascadingReindexingTemplate.java | 13 +- .../CascadingReindexingTemplateTest.java | 149 ++++++++++++++++++ 2 files changed, 156 insertions(+), 6 deletions(-) create mode 100644 indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index b4aaf4e2e1e8..208a9f75a9f9 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -144,12 +144,19 @@ public int getTaskPriority() return taskPriority; } + @JsonProperty @Override public long getInputSegmentSizeBytes() { return inputSegmentSizeBytes; } + @Override + public String getType() + { + return TYPE; + } + @JsonProperty private ReindexingRuleProvider getRuleProvider() { @@ -421,12 +428,6 @@ private List createJobsForSearchInterval( ); } - @Override - public String getType() - { - return TYPE; - } - // Legacy fields from DataSourceCompactionConfig that are not used by this template @Nullable diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java new file mode 100644 index 000000000000..1b7978824857 --- /dev/null +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -0,0 +1,149 @@ +/* + * 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.indexing.compact; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMap; +import org.apache.druid.guice.SupervisorModule; +import org.apache.druid.indexer.CompactionEngine; +import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingGranularityRule; +import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.testing.InitializedNullHandlingTest; +import org.easymock.EasyMock; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +public class CascadingReindexingTemplateTest extends InitializedNullHandlingTest +{ + private static final ObjectMapper OBJECT_MAPPER = new DefaultObjectMapper(); + + @Before + public void setUp() + { + OBJECT_MAPPER.registerModules(new SupervisorModule().getJacksonModules()); + } + + @Test + public void test_serde() throws Exception + { + final CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDataSource", + 50, + 1000000L, + InlineReindexingRuleProvider.builder() + .granularityRules(List.of( + new ReindexingGranularityRule( + "hourRule", + null, + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ), + new ReindexingGranularityRule( + "dayRule", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ) + )) + .build(), + CompactionEngine.NATIVE, + ImmutableMap.of("context_key", "context_value") + ); + + final String json = OBJECT_MAPPER.writeValueAsString(template); + final CascadingReindexingTemplate fromJson = OBJECT_MAPPER.readValue(json, CascadingReindexingTemplate.class); + + Assert.assertEquals(template.getDataSource(), fromJson.getDataSource()); + Assert.assertEquals(template.getTaskPriority(), fromJson.getTaskPriority()); + Assert.assertEquals(template.getInputSegmentSizeBytes(), fromJson.getInputSegmentSizeBytes()); + Assert.assertEquals(template.getEngine(), fromJson.getEngine()); + Assert.assertEquals(template.getTaskContext(), fromJson.getTaskContext()); + Assert.assertEquals(template.getType(), fromJson.getType()); + } + + @Test + public void test_serde_asDataSourceCompactionConfig() throws Exception + { + final CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDataSource", + 30, + 500000L, + InlineReindexingRuleProvider.builder() + .granularityRules(List.of( + new ReindexingGranularityRule( + "rule1", + null, + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ) + )) + .build(), + CompactionEngine.MSQ, + ImmutableMap.of("key", "value") + ); + + // Serialize and deserialize as DataSourceCompactionConfig interface + final String json = OBJECT_MAPPER.writeValueAsString(template); + final DataSourceCompactionConfig fromJson = OBJECT_MAPPER.readValue(json, DataSourceCompactionConfig.class); + + Assert.assertTrue(fromJson instanceof CascadingReindexingTemplate); + final CascadingReindexingTemplate cascadingFromJson = (CascadingReindexingTemplate) fromJson; + + Assert.assertEquals("testDataSource", cascadingFromJson.getDataSource()); + Assert.assertEquals(30, cascadingFromJson.getTaskPriority()); + Assert.assertEquals(500000L, cascadingFromJson.getInputSegmentSizeBytes()); + Assert.assertEquals(CompactionEngine.MSQ, cascadingFromJson.getEngine()); + Assert.assertEquals(ImmutableMap.of("key", "value"), cascadingFromJson.getTaskContext()); + Assert.assertEquals(CascadingReindexingTemplate.TYPE, cascadingFromJson.getType()); + } + + @Test + public void test_createCompactionJobs_ruleProviderNotReady() + { + final ReindexingRuleProvider notReadyProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.expect(notReadyProvider.isReady()).andReturn(false); + EasyMock.expect(notReadyProvider.getType()).andReturn("mock-provider"); + EasyMock.replay(notReadyProvider); + + final CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDataSource", + null, + null, + notReadyProvider, + null, + null + ); + + // Call createCompactionJobs - should return empty list without processing + final List jobs = template.createCompactionJobs(null, null); + + Assert.assertTrue(jobs.isEmpty()); + EasyMock.verify(notReadyProvider); + } +} From bdb514ec70ecdc4429ae2a5df1d4ee8acff7e6c5 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 15 Jan 2026 18:11:22 -0600 Subject: [PATCH 05/90] stop using forbidden apis --- .../ComposingReindexingRuleProviderTest.java | 6 +++-- .../InlineReindexingRuleProviderTest.java | 12 ++++++---- .../ReindexingDimensionsRuleTest.java | 12 ++++++---- .../compaction/ReindexingFilterRuleTest.java | 24 ++++++++++--------- .../ReindexingGranularityRuleTest.java | 12 ++++++---- .../ReindexingIOConfigRuleTest.java | 12 ++++++---- .../compaction/ReindexingMetricsRuleTest.java | 12 ++++++---- .../ReindexingProjectionRuleTest.java | 12 ++++++---- .../ReindexingTuningConfigRuleTest.java | 12 ++++++---- 9 files changed, 66 insertions(+), 48 deletions(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index e75024f13816..8ff4da8c8c80 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -20,6 +20,8 @@ package org.apache.druid.server.compaction; import com.google.common.collect.ImmutableList; +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.query.filter.SelectorDimFilter; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; @@ -35,7 +37,7 @@ public class ComposingReindexingRuleProviderTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); @Test public void test_constructor_nullProviders_throwsNullPointerException() @@ -193,7 +195,7 @@ public void test_getGranularityRules_firstWins_returnsFirstNonEmpty() @Test public void test_getFilterRulesWithInterval_firstWins_delegatesToFirstProvider() { - Interval interval = new Interval("2025-11-01T00:00:00Z/2025-11-15T00:00:00Z"); + Interval interval = Intervals.of("2025-11-01T00:00:00Z/2025-11-15T00:00:00Z"); ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(30)); ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 216d30d1dd07..c3b1c0dd20ce 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +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.query.filter.SelectorDimFilter; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; @@ -33,22 +35,22 @@ public class InlineReindexingRuleProviderTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); // Test intervals - private static final Interval INTERVAL_100_DAYS_OLD = new Interval( + private static final Interval INTERVAL_100_DAYS_OLD = Intervals.of( "2025-09-01T00:00:00Z/2025-09-02T00:00:00Z" ); // Ends 109 days before reference time - private static final Interval INTERVAL_50_DAYS_OLD = new Interval( + private static final Interval INTERVAL_50_DAYS_OLD = Intervals.of( "2025-10-20T00:00:00Z/2025-10-21T00:00:00Z" ); // Ends 59 days before reference time - private static final Interval INTERVAL_20_DAYS_OLD = new Interval( + private static final Interval INTERVAL_20_DAYS_OLD = Intervals.of( "2025-11-20T00:00:00Z/2025-11-21T00:00:00Z" ); // Ends 28 days before reference time - private static final Interval INTERVAL_5_DAYS_OLD = new Interval( + private static final Interval INTERVAL_5_DAYS_OLD = Intervals.of( "2025-12-13T00:00:00Z/2025-12-14T00:00:00Z" ); // Ends 5 days before reference time diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java index d2f365838283..2983508a8498 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -28,7 +30,7 @@ public class ReindexingDimensionsRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_14_DAYS = Period.days(14); private final ReindexingDimensionsRule rule = new ReindexingDimensionsRule( @@ -50,7 +52,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) // Interval ends at 2025-12-03, which is fully before threshold - Interval interval = new Interval("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); + Interval interval = Intervals.of("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -62,7 +64,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); + Interval interval = Intervals.of("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -74,7 +76,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); + Interval interval = Intervals.of("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -86,7 +88,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); + Interval interval = Intervals.of("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index 5ffd19a45451..85c43ea8f175 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.SelectorDimFilter; import org.joda.time.DateTime; @@ -29,7 +31,7 @@ public class ReindexingFilterRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); @@ -52,7 +54,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) // Interval ends at 2025-11-15, which is fully before threshold - Interval interval = new Interval("2025-11-14T00:00:00Z/2025-11-15T00:00:00Z"); + Interval interval = Intervals.of("2025-11-14T00:00:00Z/2025-11-15T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -64,7 +66,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-11-18T12:00:00Z/2025-11-19T12:00:00Z"); + Interval interval = Intervals.of("2025-11-18T12:00:00Z/2025-11-19T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -76,7 +78,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-11-18T00:00:00Z/2025-11-20T00:00:00Z"); + Interval interval = Intervals.of("2025-11-18T00:00:00Z/2025-11-20T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -88,7 +90,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-11-19T12:00:00Z (30 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -270,21 +272,21 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() ); // Interval ending before 6-month threshold - should be FULL - Interval beforeThreshold = new Interval("2025-06-01T00:00:00Z/2025-06-15T00:00:00Z"); + Interval beforeThreshold = Intervals.of("2025-06-01T00:00:00Z/2025-06-15T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.FULL, monthRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 6-month threshold - should be PARTIAL - Interval spanningThreshold = new Interval("2025-06-15T00:00:00Z/2025-07-15T00:00:00Z"); + Interval spanningThreshold = Intervals.of("2025-06-15T00:00:00Z/2025-07-15T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.PARTIAL, monthRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 6-month threshold - should be NONE - Interval afterThreshold = new Interval("2025-07-01T00:00:00Z/2025-07-15T00:00:00Z"); + Interval afterThreshold = Intervals.of("2025-07-01T00:00:00Z/2025-07-15T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.NONE, monthRule.appliesTo(afterThreshold, REFERENCE_TIME) @@ -308,21 +310,21 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() ); // Interval ending before 1-year threshold - should be FULL - Interval beforeThreshold = new Interval("2024-11-01T00:00:00Z/2024-12-01T00:00:00Z"); + Interval beforeThreshold = Intervals.of("2024-11-01T00:00:00Z/2024-12-01T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.FULL, yearRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 1-year threshold - should be PARTIAL - Interval spanningThreshold = new Interval("2024-12-01T00:00:00Z/2025-01-01T00:00:00Z"); + Interval spanningThreshold = Intervals.of("2024-12-01T00:00:00Z/2025-01-01T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.PARTIAL, yearRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 1-year threshold - should be NONE - Interval afterThreshold = new Interval("2025-01-01T00:00:00Z/2025-02-01T00:00:00Z"); + Interval afterThreshold = Intervals.of("2025-01-01T00:00:00Z/2025-02-01T00:00:00Z"); Assert.assertEquals( ReindexingRule.AppliesToMode.NONE, yearRule.appliesTo(afterThreshold, REFERENCE_TIME) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java index 6c6f6d3a75a0..d7ce293ec8af 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +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.server.coordinator.UserCompactionTaskGranularityConfig; import org.joda.time.DateTime; @@ -29,7 +31,7 @@ public class ReindexingGranularityRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_7_DAYS = Period.days(7); private final ReindexingGranularityRule rule = new ReindexingGranularityRule( @@ -51,7 +53,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) // Interval ends at 2025-12-10, which is fully before threshold - Interval interval = new Interval("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); + Interval interval = Intervals.of("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -63,7 +65,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); + Interval interval = Intervals.of("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -75,7 +77,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); + Interval interval = Intervals.of("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -87,7 +89,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); + Interval interval = Intervals.of("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index a690a2b5c4af..b879b22e7fc1 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -28,7 +30,7 @@ public class ReindexingIOConfigRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_60_DAYS = Period.days(60); private final ReindexingIOConfigRule rule = new ReindexingIOConfigRule( @@ -50,7 +52,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) // Interval ends at 2025-10-15, which is fully before threshold - Interval interval = new Interval("2025-10-14T00:00:00Z/2025-10-15T00:00:00Z"); + Interval interval = Intervals.of("2025-10-14T00:00:00Z/2025-10-15T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -62,7 +64,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-10-19T12:00:00Z/2025-10-20T12:00:00Z"); + Interval interval = Intervals.of("2025-10-19T12:00:00Z/2025-10-20T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -74,7 +76,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-10-19T00:00:00Z/2025-10-21T00:00:00Z"); + Interval interval = Intervals.of("2025-10-19T00:00:00Z/2025-10-21T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -86,7 +88,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-10-20T12:00:00Z (60 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java index ee20986d7903..4db355c845e6 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; @@ -30,7 +32,7 @@ public class ReindexingMetricsRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_90_DAYS = Period.days(90); private final AggregatorFactory[] testMetrics = new AggregatorFactory[]{ @@ -57,7 +59,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) // Interval ends at 2025-09-15, which is fully before threshold - Interval interval = new Interval("2025-09-14T00:00:00Z/2025-09-15T00:00:00Z"); + Interval interval = Intervals.of("2025-09-14T00:00:00Z/2025-09-15T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -69,7 +71,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-09-19T12:00:00Z/2025-09-20T12:00:00Z"); + Interval interval = Intervals.of("2025-09-19T12:00:00Z/2025-09-20T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -81,7 +83,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-09-19T00:00:00Z/2025-09-21T00:00:00Z"); + Interval interval = Intervals.of("2025-09-19T00:00:00Z/2025-09-21T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -93,7 +95,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-01T00:00:00Z/2025-12-02T00:00:00Z"); + Interval interval = Intervals.of("2025-12-01T00:00:00Z/2025-12-02T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java index 6a37ddb78c8e..3f1566bdcfcd 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java @@ -19,6 +19,8 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -29,7 +31,7 @@ public class ReindexingProjectionRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_45_DAYS = Period.days(45); private final ReindexingProjectionRule rule = new ReindexingProjectionRule( @@ -51,7 +53,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) // Interval ends at 2025-11-01, which is fully before threshold - Interval interval = new Interval("2025-10-31T00:00:00Z/2025-11-01T00:00:00Z"); + Interval interval = Intervals.of("2025-10-31T00:00:00Z/2025-11-01T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -63,7 +65,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-11-03T12:00:00Z/2025-11-04T12:00:00Z"); + Interval interval = Intervals.of("2025-11-03T12:00:00Z/2025-11-04T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -75,7 +77,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-11-03T00:00:00Z/2025-11-05T00:00:00Z"); + Interval interval = Intervals.of("2025-11-03T00:00:00Z/2025-11-05T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -87,7 +89,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index 1630a9416867..f7bde288a74e 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -20,6 +20,8 @@ package org.apache.druid.server.compaction; import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -29,7 +31,7 @@ public class ReindexingTuningConfigRuleTest { - private static final DateTime REFERENCE_TIME = new DateTime("2025-12-19T12:00:00Z"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_21_DAYS = Period.days(21); private final ReindexingTuningConfigRule rule = new ReindexingTuningConfigRule( @@ -52,7 +54,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) // Interval ends at 2025-11-25, which is fully before threshold - Interval interval = new Interval("2025-11-24T00:00:00Z/2025-11-25T00:00:00Z"); + Interval interval = Intervals.of("2025-11-24T00:00:00Z/2025-11-25T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -64,7 +66,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() { // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = new Interval("2025-11-27T12:00:00Z/2025-11-28T12:00:00Z"); + Interval interval = Intervals.of("2025-11-27T12:00:00Z/2025-11-28T12:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -76,7 +78,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() { // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) // Interval starts before threshold and ends after - PARTIAL - Interval interval = new Interval("2025-11-27T00:00:00Z/2025-11-29T00:00:00Z"); + Interval interval = Intervals.of("2025-11-27T00:00:00Z/2025-11-29T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); @@ -88,7 +90,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() { // Threshold is 2025-11-28T12:00:00Z (21 days before reference time) // Interval starts after threshold - NONE - Interval interval = new Interval("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); From 0f71da237d050093af6e7e56f036fab9714ac51c Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 16 Jan 2026 20:44:58 -0600 Subject: [PATCH 06/90] working on some test coverage --- .../compact/CascadingReindexingTemplate.java | 4 +- .../CascadingReindexingTemplateTest.java | 259 ++++++++++++++++++ 2 files changed, 262 insertions(+), 1 deletion(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 208a9f75a9f9..2b0b4250ca51 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import org.apache.druid.data.input.impl.AggregateProjectionSpec; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; @@ -277,7 +278,8 @@ public List createCompactionJobs( * and the HOUR-granularity rule starts cleanly at a day boundary. * */ - private List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) + @VisibleForTesting + static List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) { List intervals = new ArrayList<>(); DateTime previousAdjustedBoundary = null; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 1b7978824857..25a30585eb20 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -24,6 +24,7 @@ import org.apache.druid.guice.SupervisorModule; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingGranularityRule; @@ -32,6 +33,8 @@ import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.testing.InitializedNullHandlingTest; import org.easymock.EasyMock; +import org.joda.time.DateTime; +import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; import org.junit.Before; @@ -146,4 +149,260 @@ public void test_createCompactionJobs_ruleProviderNotReady() Assert.assertTrue(jobs.isEmpty()); EasyMock.verify(notReadyProvider); } + + @Test + public void test_generateIntervalsFromPeriods_noGranularityRules() + { + // Test with multiple periods but no granularity rules - should use raw period calculations + final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); + final List periods = List.of(Period.days(7), Period.days(30), Period.days(90)); + final List granularityRules = List.of(); + + final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + periods, + referenceTime, + granularityRules + ); + + // Verify we get 3 intervals + Assert.assertEquals(3, intervals.size()); + + // Interval 0: [now-30d, now-7d) + Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), intervals.get(0).getEnd()); + + // Interval 1: [now-90d, now-30d) + Assert.assertEquals(new DateTime("2024-10-18T12:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(1).getEnd()); + + // Interval 2: [MIN, now-90d) + Assert.assertEquals(DateTimes.MIN, intervals.get(2).getStart()); + Assert.assertEquals(new DateTime("2024-10-18T12:00:00Z"), intervals.get(2).getEnd()); + + // Verify no gaps between intervals + assertNoGapsBetweenIntervals(intervals); + } + + @Test + public void test_generateIntervalsFromPeriods_singleGranularityRule() + { + // Test with one granularity rule - should NOT adjust because we need both adjacent rules + final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); + final List periods = List.of(Period.days(7), Period.days(30)); + + // Only P7D has a granularity rule + final List granularityRules = List.of( + new ReindexingGranularityRule( + "hourRule", + null, + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ) + ); + + final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + periods, + referenceTime, + granularityRules + ); + + // Verify we get 2 intervals + Assert.assertEquals(2, intervals.size()); + + // No adjustment should happen because P30D doesn't have a granularity rule + // Interval 0: [now-30d, now-7d) - raw calculation + Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), intervals.get(0).getEnd()); + + // Interval 1: [MIN, now-30d) + Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); + Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(1).getEnd()); + + // Verify no gaps + assertNoGapsBetweenIntervals(intervals); + } + + @Test + public void test_generateIntervalsFromPeriods_multipleGranularityRules_verifyNoBoundaryShift() + { + // Test when both rules have granularities but calculated boundary is already aligned + // Reference time chosen so that now-30d falls exactly on a day boundary + final DateTime referenceTime = new DateTime("2025-01-16T00:00:00Z"); // Midnight + final List periods = List.of(Period.days(7), Period.days(30)); + + final List granularityRules = List.of( + new ReindexingGranularityRule( + "hourRule", + null, + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ), + new ReindexingGranularityRule( + "dayRule", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ) + ); + + final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + periods, + referenceTime, + granularityRules + ); + + Assert.assertEquals(2, intervals.size()); + + // calculatedStartTime = 2024-12-17T00:00:00Z (already at day boundary) + // beforeRuleEffectiveEnd = DAY.bucketStart(2024-12-17T00:00:00Z) = 2024-12-17T00:00:00Z + // possibleStartTime = HOUR.bucketStart(2024-12-17T00:00:00Z) = 2024-12-17T00:00:00Z + // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = false, so no increment + Assert.assertEquals(new DateTime("2024-12-17T00:00:00Z"), intervals.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T00:00:00Z"), intervals.get(0).getEnd()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); + Assert.assertEquals(new DateTime("2024-12-17T00:00:00Z"), intervals.get(1).getEnd()); + + assertNoGapsBetweenIntervals(intervals); + } + + @Test + public void test_generateIntervalsFromPeriods_multipleGranularityRules_verifyBoundaryShift() + { + // Test when both rules have granularities and boundary needs adjustment + // Use coarser granularity (DAY) for the current rule and finer (HOUR) for the before rule + final DateTime referenceTime = new DateTime("2025-01-16T12:30:00Z"); // Mid-day + final List periods = List.of(Period.days(7), Period.days(30)); + + final List granularityRules = List.of( + new ReindexingGranularityRule( + "dayRule", + null, + Period.days(7), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ), + new ReindexingGranularityRule( + "hourRule", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ) + ); + + final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + periods, + referenceTime, + granularityRules + ); + + Assert.assertEquals(2, intervals.size()); + + // calculatedStartTime = 2024-12-17T12:30:00Z (mid-day) + // beforeRuleEffectiveEnd = HOUR.bucketStart(2024-12-17T12:30:00Z) = 2024-12-17T12:00:00Z + // possibleStartTime = DAY.bucketStart(2024-12-17T12:00:00Z) = 2024-12-17T00:00:00Z + // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = true (00:00 < 12:00) + // start = DAY.increment(2024-12-17T00:00:00Z) = 2024-12-18T00:00:00Z + Assert.assertEquals(new DateTime("2024-12-18T00:00:00Z"), intervals.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T12:30:00Z"), intervals.get(0).getEnd()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); + Assert.assertEquals(new DateTime("2024-12-18T00:00:00Z"), intervals.get(1).getEnd()); + + assertNoGapsBetweenIntervals(intervals); + } + + @Test + public void test_generateIntervalsFromPeriods_edgeCases_emptyAndSinglePeriod() + { + final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); + + // Empty periods list should return empty intervals + final List emptyIntervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + List.of(), + referenceTime, + List.of() + ); + Assert.assertEquals(0, emptyIntervals.size()); + + // Single period should create one interval ending at MIN + final List singleInterval = CascadingReindexingTemplate.generateIntervalsFromPeriods( + List.of(Period.days(7)), + referenceTime, + List.of() + ); + Assert.assertEquals(1, singleInterval.size()); + Assert.assertEquals(DateTimes.MIN, singleInterval.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), singleInterval.get(0).getEnd()); + } + + @Test + public void test_generateIntervalsFromPeriods_currentRuleNull_beforeRuleExists_withPropagation() + { + // Test with 3 periods where only the 2nd and 3rd periods have granularity rules + final DateTime referenceTime = new DateTime("2025-01-16T14:30:00Z"); + final List periods = List.of(Period.days(7), Period.days(30), Period.days(90)); + + // Only P30D and P90D have granularity rules (P7D does not) + final List granularityRules = List.of( + new ReindexingGranularityRule( + "dayRule", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + ), + new ReindexingGranularityRule( + "monthRule", + null, + Period.days(90), + new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) + ) + ); + + final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( + periods, + referenceTime, + granularityRules + ); + + Assert.assertEquals(3, intervals.size()); + + // Interval 0: [adjusted, now-7d) + // currentRule=null (P7D has no rule), beforeRule=dayRule (P30D) + // Should use raw calculated time since currentRule is null + Assert.assertEquals(new DateTime("2024-12-17T14:30:00Z"), intervals.get(0).getStart()); + Assert.assertEquals(new DateTime("2025-01-09T14:30:00Z"), intervals.get(0).getEnd()); + + // Interval 1: [adjusted, now-30d) + // currentRule=dayRule (P30D), beforeRule=monthRule (P90D) + // calculatedStartTime = 2024-10-18T14:30:00Z + // beforeRuleEffectiveEnd = MONTH.bucketStart(2024-10-18T14:30:00Z) = 2024-10-01T00:00:00Z + // possibleStartTime = DAY.bucketStart(2024-10-01T00:00:00Z) = 2024-10-01T00:00:00Z + // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = false, so no increment + // IMPORTANT: end should be the previousAdjustedBoundary from interval 0 (raw calc since no adjustment) + Assert.assertEquals(new DateTime("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(new DateTime("2024-12-17T14:30:00Z"), intervals.get(1).getEnd()); + + // Interval 2: [MIN, adjusted) + // end should be the adjusted start from interval 1 + Assert.assertEquals(DateTimes.MIN, intervals.get(2).getStart()); + Assert.assertEquals(new DateTime("2024-10-01T00:00:00Z"), intervals.get(2).getEnd()); + + assertNoGapsBetweenIntervals(intervals); + } + + /** + * Asserts that there are no gaps between consecutive intervals. + * Each interval's start should equal the previous interval's end. + */ + private void assertNoGapsBetweenIntervals(List intervals) + { + for (int i = 0; i < intervals.size() - 1; i++) { + Assert.assertEquals( + "Gap detected between interval " + i + " and " + (i + 1), + intervals.get(i).getStart(), + intervals.get(i + 1).getEnd() + ); + } + } + } From acad4782a4a33b31453e8002e6ef0548a7a09822 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 20 Jan 2026 09:02:12 -0600 Subject: [PATCH 07/90] simplify search interval creation and enhance embedded test for cascading reindex --- .../compact/CompactionSupervisorTest.java | 69 ++++- .../compact/CascadingReindexingTemplate.java | 78 +----- .../CascadingReindexingTemplateTest.java | 256 ------------------ .../server/compaction/CompactionStatus.java | 6 +- 4 files changed, 74 insertions(+), 335 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 649ef743116b..7cb88c5439d4 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -21,6 +21,7 @@ import org.apache.druid.catalog.guice.CatalogClientModule; import org.apache.druid.catalog.guice.CatalogCoordinatorModule; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.druid.common.utils.IdUtils; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; @@ -30,15 +31,20 @@ import org.apache.druid.indexing.overlord.Segments; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.StringUtils; +import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.DruidMetrics; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.query.http.ClientSqlQuery; import org.apache.druid.rpc.UpdateResponse; import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingFilterRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; @@ -64,6 +70,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -317,13 +324,13 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac "hourRule", "Compact to HOUR granularity for data older than 1 days", Period.days(1), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, false) ); ReindexingGranularityRule dayRule = new ReindexingGranularityRule( "dayRule", - "Compact to DAY granularity for data older than 2 days", + "Compact to DAY granularity for data older than 7 days", Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) ); ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( @@ -353,9 +360,17 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ) ); + ReindexingFilterRule filterRule = new ReindexingFilterRule( + "filterRule", + "Drop rows where item is 'hat'", + Period.days(7), + new SelectorDimFilter("item", "hat", null) + ); + InlineReindexingRuleProvider ruleProvider = InlineReindexingRuleProvider.builder() .granularityRules(List.of(hourRule, dayRule)) .tuningConfigRules(List.of(tuningConfigRule)) + .filterRules(List.of(filterRule)) .build(); CascadingReindexingTemplate cascadingReindexingTemplate = new CascadingReindexingTemplate( @@ -372,6 +387,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac Assertions.assertEquals(4, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); Assertions.assertEquals(5, getNumSegmentsWith(Granularities.HOUR)); Assertions.assertEquals(7, getNumSegmentsWith(Granularities.DAY)); + verifyEventCountOlderThan(Period.days(7), "item", "hat", 0); } private String generateEventsInInterval(Interval interval, int numEvents, long spacingMillis) @@ -383,7 +399,9 @@ private String generateEventsInInterval(Interval interval, int numEvents, long s if (eventTime.isAfter(interval.getEnd())) { throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); } - events.add(eventTime + ",item" + i + "," + (100 + i * 5)); + String item = (i % 2 == 1) ? "hat" : "shoes"; + int metricValue = 100 + i * 5; + events.add(eventTime + "," + item + "," + metricValue); } return String.join("\n", events); @@ -495,4 +513,47 @@ public static List getEngine() { return List.of(CompactionEngine.NATIVE, CompactionEngine.MSQ); } + + private void verifyEventCountOlderThan(Period period, String dimension, String value, int expectedCount) + { + DateTime now = DateTimes.nowUtc(); + DateTime threshold = now.minus(period); + + ClientSqlQuery query = new ClientSqlQuery( + StringUtils.format( + "SELECT COUNT(*) as cnt FROM \"%s\" WHERE %s = '%s' AND __time < MILLIS_TO_TIMESTAMP(%d)", + dataSource, + dimension, + value, + threshold.getMillis() + ), + null, + false, + false, + false, + null, + null + ); + + final String resultAsJson = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(query)); + + List> result = JacksonUtils.readValue( + new DefaultObjectMapper(), + resultAsJson.getBytes(StandardCharsets.UTF_8), + new TypeReference<>() {} + ); + + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals( + expectedCount, + result.get(0).get("cnt"), + StringUtils.format( + "Expected %d events where %s='%s' older than %s", + expectedCount, + dimension, + value, + period + ) + ); + } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 2b0b4250ca51..08403c0986f4 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -221,8 +221,7 @@ public List createCompactionJobs( return Collections.emptyList(); } - // Generate intervals from periods and create jobs for each - List intervals = generateIntervalsFromPeriods(sortedPeriods, currentTime, ruleProvider.getGranularityRules()); + List intervals = generateSearchIntervals(sortedPeriods, currentTime); for (Interval reindexingInterval : intervals) { InlineSchemaDataSourceCompactionConfig.Builder builder = InlineSchemaDataSourceCompactionConfig.builder() .forDataSource(dataSource) @@ -236,7 +235,7 @@ public List createCompactionJobs( int ruleCount = applyRulesToBuilder(builder, reindexingInterval, currentTime); if (ruleCount > 0) { - LOG.info("Creating reindexing jobs for interval[%s] with %d rules", reindexingInterval, ruleCount); + LOG.info("Creating reindexing jobs for interval[%s] with [%d] rules selected", reindexingInterval, ruleCount); allJobs.addAll( createJobsForSearchInterval( new CompactionConfigBasedJobTemplate(builder.build(), createCascadingFinalizer()), @@ -252,82 +251,17 @@ public List createCompactionJobs( return allJobs; } - /** - * Generates cascading intervals from sorted periods. - *

- * For periods [P7D, P30D, P90D], generates intervals: - *

    - *
  • [now-30d, now-7d)
  • - *
  • [now-90d, now-30d)
  • - *
  • [DateTimes.MIN, now-90d)
  • - *
- *

- * If adjacent rules have segment granularities defined, boundaries are adjusted - * to align with granularity buckets to prevent gaps in the timeline. Example: - *

-   * Given P7D rule with HOUR granularity and P30D rule with DAY granularity,
-   * and referenceTime = 2025-12-19 14:37:22:
-   *
-   * Without adjustment:
-   *   Calculated boundary: 2025-11-19 14:37:22 (now - 30 days)
-   *
-   * With adjustment:
-   *   Aligned boundary: 2025-11-20 00:00:00 (aligned to start of next day)
-   *
-   * This ensures the DAY-granularity rule creates complete day-aligned segments
-   * and the HOUR-granularity rule starts cleanly at a day boundary.
-   * 
- */ - @VisibleForTesting - static List generateIntervalsFromPeriods(List sortedPeriods, DateTime referenceTime, List granularityRules) + private List generateSearchIntervals(List sortedPeriods, DateTime referenceTime) { - List intervals = new ArrayList<>(); - DateTime previousAdjustedBoundary = null; - + List intervals = new ArrayList<>(sortedPeriods.size()); for (int i = 0; i < sortedPeriods.size(); i++) { - // End is either the previous adjusted boundary, or the raw calculation for the first interval - DateTime end = previousAdjustedBoundary != null - ? previousAdjustedBoundary - : referenceTime.minus(sortedPeriods.get(i)); + DateTime end = referenceTime.minus(sortedPeriods.get(i)); DateTime start; - if (i + 1 < sortedPeriods.size()) { - // Bounded interval: between two periods - // We may need to adjust the start time to avoid gaps if both adjacent rules have segment granularities defined. - final DateTime calculatedStartTime = referenceTime.minus(sortedPeriods.get(i + 1)); - final int finalI = i; - ReindexingGranularityRule currentRule = granularityRules.stream() - .filter(rule -> - rule.getPeriod().equals(sortedPeriods.get(finalI)) && rule.getGranularityConfig().getSegmentGranularity() != null) - .findFirst() - .orElse(null); - ReindexingGranularityRule beforeRule = granularityRules.stream() - .filter(rule -> - rule.getPeriod().equals(sortedPeriods.get(finalI + 1)) && rule.getGranularityConfig().getSegmentGranularity() != null) - .findFirst() - .orElse(null); - - if (currentRule == null || beforeRule == null) { - start = calculatedStartTime; - } else { - final Granularity granularity = currentRule.getGranularityConfig().getSegmentGranularity(); - final Granularity beforeGranularity = beforeRule.getGranularityConfig().getSegmentGranularity(); - - final DateTime beforeRuleEffectiveEnd = beforeGranularity.bucketStart(calculatedStartTime); - final DateTime possibleStartTime = granularity.bucketStart(beforeRuleEffectiveEnd); - start = possibleStartTime.isBefore(beforeRuleEffectiveEnd) - ? granularity.increment(possibleStartTime) - : possibleStartTime; - } - - // Save the adjusted start boundary for the next (older) interval's end - previousAdjustedBoundary = start; + start = referenceTime.minus(sortedPeriods.get(i + 1)); } else { - // Unbounded interval: from the earliest time. This allows the last rule to cover all remaining segments - // into the past start = DateTimes.MIN; } - intervals.add(new Interval(start, end)); } return intervals; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 25a30585eb20..6dfb7c8a396c 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -149,260 +149,4 @@ public void test_createCompactionJobs_ruleProviderNotReady() Assert.assertTrue(jobs.isEmpty()); EasyMock.verify(notReadyProvider); } - - @Test - public void test_generateIntervalsFromPeriods_noGranularityRules() - { - // Test with multiple periods but no granularity rules - should use raw period calculations - final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); - final List periods = List.of(Period.days(7), Period.days(30), Period.days(90)); - final List granularityRules = List.of(); - - final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - periods, - referenceTime, - granularityRules - ); - - // Verify we get 3 intervals - Assert.assertEquals(3, intervals.size()); - - // Interval 0: [now-30d, now-7d) - Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), intervals.get(0).getEnd()); - - // Interval 1: [now-90d, now-30d) - Assert.assertEquals(new DateTime("2024-10-18T12:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(1).getEnd()); - - // Interval 2: [MIN, now-90d) - Assert.assertEquals(DateTimes.MIN, intervals.get(2).getStart()); - Assert.assertEquals(new DateTime("2024-10-18T12:00:00Z"), intervals.get(2).getEnd()); - - // Verify no gaps between intervals - assertNoGapsBetweenIntervals(intervals); - } - - @Test - public void test_generateIntervalsFromPeriods_singleGranularityRule() - { - // Test with one granularity rule - should NOT adjust because we need both adjacent rules - final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); - final List periods = List.of(Period.days(7), Period.days(30)); - - // Only P7D has a granularity rule - final List granularityRules = List.of( - new ReindexingGranularityRule( - "hourRule", - null, - Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ) - ); - - final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - periods, - referenceTime, - granularityRules - ); - - // Verify we get 2 intervals - Assert.assertEquals(2, intervals.size()); - - // No adjustment should happen because P30D doesn't have a granularity rule - // Interval 0: [now-30d, now-7d) - raw calculation - Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), intervals.get(0).getEnd()); - - // Interval 1: [MIN, now-30d) - Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); - Assert.assertEquals(new DateTime("2024-12-17T12:00:00Z"), intervals.get(1).getEnd()); - - // Verify no gaps - assertNoGapsBetweenIntervals(intervals); - } - - @Test - public void test_generateIntervalsFromPeriods_multipleGranularityRules_verifyNoBoundaryShift() - { - // Test when both rules have granularities but calculated boundary is already aligned - // Reference time chosen so that now-30d falls exactly on a day boundary - final DateTime referenceTime = new DateTime("2025-01-16T00:00:00Z"); // Midnight - final List periods = List.of(Period.days(7), Period.days(30)); - - final List granularityRules = List.of( - new ReindexingGranularityRule( - "hourRule", - null, - Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ), - new ReindexingGranularityRule( - "dayRule", - null, - Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ) - ); - - final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - periods, - referenceTime, - granularityRules - ); - - Assert.assertEquals(2, intervals.size()); - - // calculatedStartTime = 2024-12-17T00:00:00Z (already at day boundary) - // beforeRuleEffectiveEnd = DAY.bucketStart(2024-12-17T00:00:00Z) = 2024-12-17T00:00:00Z - // possibleStartTime = HOUR.bucketStart(2024-12-17T00:00:00Z) = 2024-12-17T00:00:00Z - // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = false, so no increment - Assert.assertEquals(new DateTime("2024-12-17T00:00:00Z"), intervals.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T00:00:00Z"), intervals.get(0).getEnd()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); - Assert.assertEquals(new DateTime("2024-12-17T00:00:00Z"), intervals.get(1).getEnd()); - - assertNoGapsBetweenIntervals(intervals); - } - - @Test - public void test_generateIntervalsFromPeriods_multipleGranularityRules_verifyBoundaryShift() - { - // Test when both rules have granularities and boundary needs adjustment - // Use coarser granularity (DAY) for the current rule and finer (HOUR) for the before rule - final DateTime referenceTime = new DateTime("2025-01-16T12:30:00Z"); // Mid-day - final List periods = List.of(Period.days(7), Period.days(30)); - - final List granularityRules = List.of( - new ReindexingGranularityRule( - "dayRule", - null, - Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ), - new ReindexingGranularityRule( - "hourRule", - null, - Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ) - ); - - final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - periods, - referenceTime, - granularityRules - ); - - Assert.assertEquals(2, intervals.size()); - - // calculatedStartTime = 2024-12-17T12:30:00Z (mid-day) - // beforeRuleEffectiveEnd = HOUR.bucketStart(2024-12-17T12:30:00Z) = 2024-12-17T12:00:00Z - // possibleStartTime = DAY.bucketStart(2024-12-17T12:00:00Z) = 2024-12-17T00:00:00Z - // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = true (00:00 < 12:00) - // start = DAY.increment(2024-12-17T00:00:00Z) = 2024-12-18T00:00:00Z - Assert.assertEquals(new DateTime("2024-12-18T00:00:00Z"), intervals.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T12:30:00Z"), intervals.get(0).getEnd()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(1).getStart()); - Assert.assertEquals(new DateTime("2024-12-18T00:00:00Z"), intervals.get(1).getEnd()); - - assertNoGapsBetweenIntervals(intervals); - } - - @Test - public void test_generateIntervalsFromPeriods_edgeCases_emptyAndSinglePeriod() - { - final DateTime referenceTime = new DateTime("2025-01-16T12:00:00Z"); - - // Empty periods list should return empty intervals - final List emptyIntervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - List.of(), - referenceTime, - List.of() - ); - Assert.assertEquals(0, emptyIntervals.size()); - - // Single period should create one interval ending at MIN - final List singleInterval = CascadingReindexingTemplate.generateIntervalsFromPeriods( - List.of(Period.days(7)), - referenceTime, - List.of() - ); - Assert.assertEquals(1, singleInterval.size()); - Assert.assertEquals(DateTimes.MIN, singleInterval.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T12:00:00Z"), singleInterval.get(0).getEnd()); - } - - @Test - public void test_generateIntervalsFromPeriods_currentRuleNull_beforeRuleExists_withPropagation() - { - // Test with 3 periods where only the 2nd and 3rd periods have granularity rules - final DateTime referenceTime = new DateTime("2025-01-16T14:30:00Z"); - final List periods = List.of(Period.days(7), Period.days(30), Period.days(90)); - - // Only P30D and P90D have granularity rules (P7D does not) - final List granularityRules = List.of( - new ReindexingGranularityRule( - "dayRule", - null, - Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ), - new ReindexingGranularityRule( - "monthRule", - null, - Period.days(90), - new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) - ) - ); - - final List intervals = CascadingReindexingTemplate.generateIntervalsFromPeriods( - periods, - referenceTime, - granularityRules - ); - - Assert.assertEquals(3, intervals.size()); - - // Interval 0: [adjusted, now-7d) - // currentRule=null (P7D has no rule), beforeRule=dayRule (P30D) - // Should use raw calculated time since currentRule is null - Assert.assertEquals(new DateTime("2024-12-17T14:30:00Z"), intervals.get(0).getStart()); - Assert.assertEquals(new DateTime("2025-01-09T14:30:00Z"), intervals.get(0).getEnd()); - - // Interval 1: [adjusted, now-30d) - // currentRule=dayRule (P30D), beforeRule=monthRule (P90D) - // calculatedStartTime = 2024-10-18T14:30:00Z - // beforeRuleEffectiveEnd = MONTH.bucketStart(2024-10-18T14:30:00Z) = 2024-10-01T00:00:00Z - // possibleStartTime = DAY.bucketStart(2024-10-01T00:00:00Z) = 2024-10-01T00:00:00Z - // possibleStartTime.isBefore(beforeRuleEffectiveEnd) = false, so no increment - // IMPORTANT: end should be the previousAdjustedBoundary from interval 0 (raw calc since no adjustment) - Assert.assertEquals(new DateTime("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(new DateTime("2024-12-17T14:30:00Z"), intervals.get(1).getEnd()); - - // Interval 2: [MIN, adjusted) - // end should be the adjusted start from interval 1 - Assert.assertEquals(DateTimes.MIN, intervals.get(2).getStart()); - Assert.assertEquals(new DateTime("2024-10-01T00:00:00Z"), intervals.get(2).getEnd()); - - assertNoGapsBetweenIntervals(intervals); - } - - /** - * Asserts that there are no gaps between consecutive intervals. - * Each interval's start should equal the previous interval's end. - */ - private void assertNoGapsBetweenIntervals(List intervals) - { - for (int i = 0; i < intervals.size() - 1; i++) { - Assert.assertEquals( - "Gap detected between interval " + i + " and " + (i + 1), - intervals.get(i).getStart(), - intervals.get(i + 1).getEnd() - ); - } - } - } diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 73806a4a2863..59e8f5d45734 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -265,9 +265,9 @@ public static CompactionStatus running(String message) * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. *

* - * @param candidateSegments - * @param expectedFilter - * @param fingerprintMapper + * @param candidateSegments the compaction candidate + * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) + * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied */ @Nullable From d49fbf4c096acb248d429c84097b04895d4b642c Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 20 Jan 2026 09:06:21 -0600 Subject: [PATCH 08/90] fixup checkstyle --- .../testing/embedded/compact/CompactionSupervisorTest.java | 6 +++--- .../druid/indexing/compact/CascadingReindexingTemplate.java | 1 - .../indexing/compact/CascadingReindexingTemplateTest.java | 3 --- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 7cb88c5439d4..21606691b5b0 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -19,9 +19,9 @@ package org.apache.druid.testing.embedded.compact; +import com.fasterxml.jackson.core.type.TypeReference; import org.apache.druid.catalog.guice.CatalogClientModule; import org.apache.druid.catalog.guice.CatalogCoordinatorModule; -import com.fasterxml.jackson.core.type.TypeReference; import org.apache.druid.common.utils.IdUtils; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; @@ -29,13 +29,13 @@ import org.apache.druid.indexing.compact.CascadingReindexingTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.Segments; +import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.StringUtils; -import org.apache.druid.java.util.common.jackson.JacksonUtils; -import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.query.DruidMetrics; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.query.http.ClientSqlQuery; diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 08403c0986f4..04da0bb909c2 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.common.annotations.VisibleForTesting; import org.apache.druid.data.input.impl.AggregateProjectionSpec; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 6dfb7c8a396c..1b7978824857 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -24,7 +24,6 @@ import org.apache.druid.guice.SupervisorModule; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.jackson.DefaultObjectMapper; -import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingGranularityRule; @@ -33,8 +32,6 @@ import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.testing.InitializedNullHandlingTest; import org.easymock.EasyMock; -import org.joda.time.DateTime; -import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; import org.junit.Before; From 5efb4bfbcf04bcd53aeb19512930776a9b878e97 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 20 Jan 2026 14:50:26 -0600 Subject: [PATCH 09/90] temporary fixup to test Using 1 row and creating 0 row segments makes the test fail for native compaction runner. I cannot reproduce in docker to figure out how the test is misconfigured --- .../testing/embedded/compact/CompactionSupervisorTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 21606691b5b0..d7f12d68162c 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -46,11 +46,13 @@ import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingFilterRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.testing.embedded.EmbeddedBroker; import org.apache.druid.testing.embedded.EmbeddedCoordinator; @@ -399,9 +401,9 @@ private String generateEventsInInterval(Interval interval, int numEvents, long s if (eventTime.isAfter(interval.getEnd())) { throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); } - String item = (i % 2 == 1) ? "hat" : "shoes"; int metricValue = 100 + i * 5; - events.add(eventTime + "," + item + "," + metricValue); + events.add(eventTime + "," + "hat" + "," + metricValue); + events.add(eventTime + "," + "shoes" + "," + metricValue); } return String.join("\n", events); From 5a87759f5a9d9f3f353bb62c857ff14a7db7e0a0 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 20 Jan 2026 14:52:19 -0600 Subject: [PATCH 10/90] fix checkstyle --- .../testing/embedded/compact/CompactionSupervisorTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index d7f12d68162c..38ad2e1f8dfe 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -46,13 +46,11 @@ import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingFilterRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; -import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.testing.embedded.EmbeddedBroker; import org.apache.druid.testing.embedded.EmbeddedCoordinator; From 0850a8fd4ef4089b4fbc14f30dc5f7d0ff364574 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 20 Jan 2026 18:23:23 -0600 Subject: [PATCH 11/90] remove native runner for one compaction supervisor test due to native issue with range dim and all rows filtered out --- .../compact/CompactionSupervisorTest.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 38ad2e1f8dfe..e73d4ae181e0 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -46,11 +46,13 @@ import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingFilterRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.testing.embedded.EmbeddedBroker; import org.apache.druid.testing.embedded.EmbeddedCoordinator; @@ -67,6 +69,7 @@ import org.joda.time.Interval; import org.joda.time.Period; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -285,10 +288,12 @@ public void test_compaction_withPersistLastCompactionStateFalse_storesOnlyFinger verifySegmentsHaveNullLastCompactionStateAndNonNullFingerprint(); } - @MethodSource("getEngine") - @ParameterizedTest(name = "compactionEngine={0}") - public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompactionRules(CompactionEngine compactionEngine) + @Test + public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompactionRules() { + // We eventually want to run with parameterized test for both engines but right now using RANGE partitioning and filtering + // out all rows with native engine cant handle right now. + CompactionEngine compactionEngine = CompactionEngine.MSQ; configureCompaction(compactionEngine); DateTime now = DateTimes.nowUtc(); @@ -367,17 +372,22 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac new SelectorDimFilter("item", "hat", null) ); - InlineReindexingRuleProvider ruleProvider = InlineReindexingRuleProvider.builder() + InlineReindexingRuleProvider.Builder ruleProvider = InlineReindexingRuleProvider.builder() .granularityRules(List.of(hourRule, dayRule)) .tuningConfigRules(List.of(tuningConfigRule)) - .filterRules(List.of(filterRule)) - .build(); + .filterRules(List.of(filterRule)); + + if (compactionEngine == CompactionEngine.NATIVE) { + ruleProvider = ruleProvider.ioConfigRules( + List.of(new ReindexingIOConfigRule("dropExisting", null, Period.days(7), new UserCompactionTaskIOConfig(true))) + ); + } CascadingReindexingTemplate cascadingReindexingTemplate = new CascadingReindexingTemplate( dataSource, null, null, - ruleProvider, + ruleProvider.build(), compactionEngine, null ); @@ -399,9 +409,9 @@ private String generateEventsInInterval(Interval interval, int numEvents, long s if (eventTime.isAfter(interval.getEnd())) { throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); } + String item = i % 2 == 0 ? "hat" : "shirt"; int metricValue = 100 + i * 5; - events.add(eventTime + "," + "hat" + "," + metricValue); - events.add(eventTime + "," + "shoes" + "," + metricValue); + events.add(eventTime + "," + item + "," + metricValue); } return String.join("\n", events); From 36674562bc17238a22817d983746194cfeca6cbb Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 09:04:58 -0600 Subject: [PATCH 12/90] refactorings from self review --- .../compact/CascadingReindexingTemplate.java | 2 +- .../indexing/compact/CompactionRule.java | 2 +- .../server/compaction/CompactionStatus.java | 113 ------ .../compaction/ReindexingFilterRule.java | 123 ++++++ .../compaction/CompactionStatusTest.java | 311 --------------- .../compaction/ReindexingFilterRuleTest.java | 368 ++++++++++++++++++ 6 files changed, 493 insertions(+), 426 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 04da0bb909c2..4495642e9d35 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -180,7 +180,7 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() config.getTransformSpec().getFilter() instanceof NotDimFilter) { // Compute the minimal set of filter rules needed for this candidate - NotDimFilter reducedTransformSpecFilter = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter reducedTransformSpecFilter = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( candidate, (NotDimFilter) config.getTransformSpec().getFilter(), params.getFingerprintMapper() diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java index f73a488821c2..10099566ea71 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionRule.java @@ -26,7 +26,7 @@ import org.joda.time.Period; /** - * A single rule used inside {@link CascadingReindexingTemplate}. + * A single rule used inside {@link CascadingCompactionTemplate}. */ public class CompactionRule { diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 59e8f5d45734..be13ec40c4e9 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -32,9 +32,6 @@ import org.apache.druid.java.util.common.granularity.GranularityType; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.transform.CompactionTransformSpec; @@ -48,12 +45,9 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -258,113 +252,6 @@ public static CompactionStatus running(String message) return new CompactionStatus(State.RUNNING, message, null, null); } - /** - * Computes the required set of filter rules to be applied for the given compaction candidate. - *

- * We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces - * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. - *

- * - * @param candidateSegments the compaction candidate - * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) - * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints - * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied - */ - @Nullable - public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( - CompactionCandidate candidateSegments, - NotDimFilter expectedFilter, - IndexingStateFingerprintMapper fingerprintMapper - ) - { - if (!(expectedFilter.getField() instanceof OrDimFilter)) { - return expectedFilter; - } - - List expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); - - // Collect unique fingerprints - Set uniqueFingerprints = candidateSegments.getSegments().stream() - .map(DataSegment::getIndexingStateFingerprint) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (uniqueFingerprints.isEmpty()) { - // no fingerprints means that no candidate segments have transforms to compare against. Return all filters eagerly. - return expectedFilter; - } - - // Accumulate filters that haven't been applied across all fingerprints - Set unappliedRules = new HashSet<>(); - - for (String fingerprint : uniqueFingerprints) { - CompactionState state = fingerprintMapper.getStateForFingerprint(fingerprint).orElse(null); - - if (state == null) { - // Safety: if state is missing, return all filters eagerly since we can't determine applied filters - return expectedFilter; - } - - // Extract applied filters from the CompactionState into a Set - Set appliedFilters = extractAppliedFilters(state); - - // If transform spec or filter for the CompactionState is null, return all expected filters eagerly - if (appliedFilters == null) { - return expectedFilter; - } - - // Check which expected filters are NOT in the applied set and add them to unappliedRules - for (DimFilter expected : expectedFilters) { - if (!appliedFilters.contains(expected)) { - unappliedRules.add(expected); - } - } - } - - log.info( - "Computed [%d] unapplied rules out of [%d] possible rules for candidate", - unappliedRules.size(), - expectedFilters.size() - ); - - // If all filters were applied, return null - if (unappliedRules.isEmpty()) { - return null; - } - - // Return the delta as NOT(OR(unapplied filters)) - return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); - } - - /** - * Extracts applied filters from a CompactionState. - * Returns null if transform spec or filter is null (indicating all filters should be applied). - */ - @Nullable - private static Set extractAppliedFilters(CompactionState state) - { - if (state.getTransformSpec() == null) { - return null; - } - - DimFilter filter = state.getTransformSpec().getFilter(); - if (filter == null) { - return null; - } - - if (!(filter instanceof NotDimFilter)) { - return Collections.emptySet(); - } - - DimFilter inner = ((NotDimFilter) filter).getField(); - - if (inner instanceof OrDimFilter) { - return new HashSet<>(((OrDimFilter) inner).getFields()); - } else { - return Collections.singleton(inner); - } - } - /** * Determines the CompactionStatus of the given candidate segments by evaluating * the {@link #CHECKS} one by one. If any check returns an incomplete status, diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index 2983a7bfa4f9..fc20c0a4612e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -21,12 +21,24 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.timeline.CompactionState; +import org.apache.druid.timeline.DataSegment; import org.joda.time.Period; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; /** * A compaction filter rule that specifies rows to remove from segments older than a specified period. @@ -58,6 +70,7 @@ */ public class ReindexingFilterRule extends AbstractReindexingRule { + private static final Logger LOG = new Logger(ReindexingFilterRule.class); private final DimFilter filter; @@ -84,4 +97,114 @@ public DimFilter getFilter() { return filter; } + + /** + * Computes the required set of filter rules to be applied for the given compaction candidate. + *

+ * We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces + * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. + *

+ * + * @param candidateSegments the compaction candidate + * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) + * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints + * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied + */ + @Nullable + public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( + CompactionCandidate candidateSegments, + NotDimFilter expectedFilter, + IndexingStateFingerprintMapper fingerprintMapper + ) + { + List expectedFilters; + if (!(expectedFilter.getField() instanceof OrDimFilter)) { + expectedFilters = Collections.singletonList(expectedFilter.getField()); + } else { + expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); + } + + // Collect unique fingerprints + Set uniqueFingerprints = candidateSegments.getSegments().stream() + .map(DataSegment::getIndexingStateFingerprint) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (uniqueFingerprints.isEmpty()) { + // no fingerprints means that no candidate segments have transforms to compare against. Return all filters eagerly. + return expectedFilter; + } + + // Accumulate filters that haven't been applied across all fingerprints + Set unappliedRules = new HashSet<>(); + + for (String fingerprint : uniqueFingerprints) { + CompactionState state = fingerprintMapper.getStateForFingerprint(fingerprint).orElse(null); + + if (state == null) { + // Safety: if state is missing, return all filters eagerly since we can't determine applied filters + return expectedFilter; + } + + // Extract applied filters from the CompactionState into a Set + Set appliedFilters = extractAppliedFilters(state); + + // If transform spec or filter for the CompactionState is null, return all expected filters eagerly + if (appliedFilters == null) { + return expectedFilter; + } + + // Check which expected filters are NOT in the applied set and add them to unappliedRules + for (DimFilter expected : expectedFilters) { + if (!appliedFilters.contains(expected)) { + unappliedRules.add(expected); + } + } + } + + LOG.debug( + "Computed [%d] unapplied rules out of [%d] possible rules for candidate", + unappliedRules.size(), + expectedFilters.size() + ); + + // If all filters were applied, return null + if (unappliedRules.isEmpty()) { + return null; + } + + // Return the delta as NOT(OR(unapplied filters)) + return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); + } + + /** + * Extracts the set of applied filters from a {@link CompactionState}. + * + * @param state the {@link CompactionState} to extract applied filters from + * @return the set of applied filters, or null if transform spec or filter is null (indicating 0 applied filters) + */ + @Nullable + private static Set extractAppliedFilters(CompactionState state) + { + if (state.getTransformSpec() == null) { + return null; + } + + DimFilter filter = state.getTransformSpec().getFilter(); + if (filter == null) { + return null; + } + + if (!(filter instanceof NotDimFilter)) { + return Collections.emptySet(); + } + + DimFilter inner = ((NotDimFilter) filter).getField(); + + if (inner instanceof OrDimFilter) { + return new HashSet<>(((OrDimFilter) inner).getFields()); + } else { + return Collections.singleton(inner); + } + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java index 446b8d3de974..2b274e805d4c 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java @@ -36,10 +36,6 @@ import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.OrDimFilter; -import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.AutoTypeColumnSchema; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.TestDataSource; @@ -49,7 +45,6 @@ import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.nested.NestedCommonFormatColumnFormatSpec; -import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -58,18 +53,12 @@ import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; -import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; public class CompactionStatusTest { @@ -895,304 +884,4 @@ private static CompactionState createCompactionStateWithGranularity(Granularity null ); } - - // ============================ - // computeRequiredSetOfFilterRulesForCandidate tests - // ============================ - - @Test - public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - NotDimFilter expectedFilter = new NotDimFilter(filterA); - - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - @Test - public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithFilters("fp1", filterA, filterB, filterC); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertNull(result); - } - - @Test - public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithoutFilters("fp1"); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Assert.assertEquals(3, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - DimFilter filterD = new SelectorDimFilter("country", "DE", null); - - CompactionState state = createStateWithFilters("fp1", filterA, filterB); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); - - Assert.assertEquals(expectedSet, resultSet); - } - - @Test - public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - DimFilter filterD = new SelectorDimFilter("country", "DE", null); - - CompactionState state1 = createStateWithFilters("fp1", filterA, filterB); - CompactionState state2 = createStateWithFilters("fp2", filterA, filterC); - DateTime now = DateTimes.nowUtc(); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); - - Assert.assertEquals(expectedSet, resultSet); - } - - @Test - public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state1 = createStateWithFilters("fp1", filterA); - CompactionState state2 = createStateWithFilters("fp2", filterA); - DateTime now = DateTimes.nowUtc(); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - - Assert.assertEquals(2, resultSet.size()); - Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - // No state persisted for fp1 - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - @Test - public void testComputeRequiredFilters_TransformSpecWithSingleFilter() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithSingleFilter("fp1", filterA); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Assert.assertEquals(2, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_SegmentsWithNoFingerprints() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionCandidate candidate = createCandidateWithNullFingerprints(3); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - - NotDimFilter result = CompactionStatus.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - // Helper methods for filter tests - - private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) - { - List segments = Arrays.stream(fingerprints) - .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) - .collect(Collectors.toList()); - return CompactionCandidate.from(segments, null); - } - - private CompactionCandidate createCandidateWithNullFingerprints(int count) - { - List segments = new ArrayList<>(); - for (int i = 0; i < count; i++) { - segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); - } - return CompactionCandidate.from(segments, null); - } - - private CompactionState createStateWithFilters(String fingerprint, DimFilter... filters) - { - OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); - NotDimFilter notFilter = new NotDimFilter(orFilter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); - - return new CompactionState( - null, - null, - null, - transformSpec, - IndexSpec.getDefault(), - null, - null - ); - } - - private CompactionState createStateWithSingleFilter(String fingerprint, DimFilter filter) - { - NotDimFilter notFilter = new NotDimFilter(filter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); - - return new CompactionState( - null, - null, - null, - transformSpec, - IndexSpec.getDefault(), - null, - null - ); - } - - private CompactionState createStateWithoutFilters(String fingerprint) - { - return new CompactionState( - null, - null, - null, - null, - IndexSpec.getDefault(), - null, - null - ); - } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index 85c43ea8f175..15d283cff92f 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -19,21 +19,47 @@ package org.apache.druid.server.compaction; +import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.TestDataSource; +import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; +import org.apache.druid.segment.metadata.HeapMemoryIndexingStateStorage; +import org.apache.druid.segment.metadata.IndexingStateCache; +import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.timeline.CompactionState; +import org.apache.druid.timeline.DataSegment; +import org.apache.druid.timeline.SegmentId; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + public class ReindexingFilterRuleTest { private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); + private static final DataSegment WIKI_SEGMENT + = DataSegment.builder(SegmentId.of(TestDataSource.WIKI, Intervals.of("2013-01-01/PT1H"), "v1", 0)) + .size(100_000_000L) + .build(); + private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); private final ReindexingFilterRule rule = new ReindexingFilterRule( "test-filter-rule", @@ -42,6 +68,21 @@ public class ReindexingFilterRuleTest testFilter ); + private HeapMemoryIndexingStateStorage indexingStateStorage; + private IndexingStateCache indexingStateCache; + private IndexingStateFingerprintMapper fingerprintMapper; + + @Before + public void setUp() + { + indexingStateStorage = new HeapMemoryIndexingStateStorage(); + indexingStateCache = new IndexingStateCache(); + fingerprintMapper = new DefaultIndexingStateFingerprintMapper( + indexingStateCache, + new DefaultObjectMapper() + ); + } + @Test public void test_isAdditive_returnsTrue() { @@ -330,4 +371,331 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() yearRule.appliesTo(afterThreshold, REFERENCE_TIME) ); } + + // ============================ + // computeRequiredSetOfFilterRulesForCandidate tests + // ============================ + + @Test + public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + NotDimFilter expectedFilter = new NotDimFilter(filterA); + + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void test_computeRequiredSetOfFilterRulesForCandidate_oneFilter_nothingToApply() + { + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + CompactionState state = createStateWithFilters(filterB); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + NotDimFilter expectedFilter = new NotDimFilter(filterB); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertNull(result); + } + + @Test + public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithFilters(filterA, filterB, filterC); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertNull(result); + } + + @Test + public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithoutFilters(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(3, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state = createStateWithFilters(filterA, filterB); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state1 = createStateWithFilters(filterA, filterB); + CompactionState state2 = createStateWithFilters(filterA, filterC); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state1 = createStateWithFilters(filterA); + CompactionState state2 = createStateWithFilters(filterA); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + + Assert.assertEquals(2, resultSet.size()); + Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + // No state persisted for fp1 + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void testComputeRequiredFilters_TransformSpecWithSingleFilter() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithSingleFilter(filterA); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(2, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_SegmentsWithNoFingerprints() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionCandidate candidate = createCandidateWithNullFingerprints(3); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + + NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + // Helper methods for filter tests + + private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) + { + List segments = Arrays.stream(fingerprints) + .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) + .collect(Collectors.toList()); + return CompactionCandidate.from(segments, null); + } + + private CompactionCandidate createCandidateWithNullFingerprints(int count) + { + List segments = new ArrayList<>(); + for (int i = 0; i < count; i++) { + segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); + } + return CompactionCandidate.from(segments, null); + } + + private CompactionState createStateWithFilters(DimFilter... filters) + { + OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); + NotDimFilter notFilter = new NotDimFilter(orFilter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithSingleFilter(DimFilter filter) + { + NotDimFilter notFilter = new NotDimFilter(filter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithoutFilters() + { + return new CompactionState( + null, + null, + null, + null, + IndexSpec.getDefault(), + null, + null + ); + } + + /** + * Helper to sync the cache with states stored in the manager (for tests that persist states). + */ + private void syncCacheFromManager() + { + indexingStateCache.resetIndexingStatesForPublishedSegments(indexingStateStorage.getAllStoredStates()); + } } From a280690eca131aa69ded6f46ef751f8ca896412e Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 12:06:47 -0600 Subject: [PATCH 13/90] Fixup naming to prefer reindexing over compaction --- .../InlineReindexingRuleProvider.java | 202 +++++++++--------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index f73840ee1804..fe3a37a50a96 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -40,7 +40,7 @@ * Rule provider that returns a static list of rules defined inline in the configuration. *

* This is the simplest provider implementation, suitable for testing and use cases where the number of rules is - * relatively small and can be defined directly in the compaction config. + * relatively small and can be defined directly in the supervisor spec. *

* When filtering rules by interval, this provider only returns rules where {@link ReindexingRule#appliesTo(Interval, DateTime)} * returns {@link ReindexingRule.AppliesToMode#FULL}. Rules with partial or no overlap are excluded. @@ -53,7 +53,7 @@ *

{@code
  * {
  *   "type": "inline",
- *   "compactionFilterRules": [
+ *   "reindexingFilterRules": [
  *     {
  *       "id": "remove-bots-90d",
  *       "period": "P90D",
@@ -88,33 +88,33 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider
 {
   public static final String TYPE = "inline";
 
-  private final List compactionFilterRules;
-  private final List compactionMetricsRules;
-  private final List compactionDimensionsRules;
-  private final List compactionIOConfigRules;
-  private final List compactionProjectionRules;
-  private final List compactionGranularityRules;
-  private final List compactionTuningConfigRules;
+  private final List reindexingFilterRules;
+  private final List reindexingMetricsRules;
+  private final List reindexingDimensionsRules;
+  private final List reindexingIOConfigRules;
+  private final List reindexingProjectionRules;
+  private final List reindexingGranularityRules;
+  private final List reindexingTuningConfigRules;
 
 
   @JsonCreator
   public InlineReindexingRuleProvider(
-      @JsonProperty("compactionFilterRules") @Nullable List compactionFilterRules,
-      @JsonProperty("compactionMetricsRules") @Nullable List compactionMetricsRules,
-      @JsonProperty("compactionDimensionsRules") @Nullable List compactionDimensionsRules,
-      @JsonProperty("compactionIOConfigRules") @Nullable List compactionIOConfigRules,
-      @JsonProperty("compactionProjectionRules") @Nullable List compactionProjectionRules,
-      @JsonProperty("compactionGranularityRules") @Nullable List compactionGranularityRules,
-      @JsonProperty("compactionTuningConfigRules") @Nullable List compactionTuningConfigRules
+      @JsonProperty("reindexingFilterRules") @Nullable List reindexingFilterRules,
+      @JsonProperty("reindexingMetricsRules") @Nullable List reindexingMetricsRules,
+      @JsonProperty("reindexingDimensionsRules") @Nullable List reindexingDimensionsRules,
+      @JsonProperty("reindexingIOConfigRules") @Nullable List reindexingIOConfigRules,
+      @JsonProperty("reindexingProjectionRules") @Nullable List reindexingProjectionRules,
+      @JsonProperty("reindexingGranularityRules") @Nullable List reindexingGranularityRules,
+      @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules
   )
   {
-    this.compactionFilterRules = Configs.valueOrDefault(compactionFilterRules, Collections.emptyList());
-    this.compactionMetricsRules = Configs.valueOrDefault(compactionMetricsRules, Collections.emptyList());
-    this.compactionDimensionsRules = Configs.valueOrDefault(compactionDimensionsRules, Collections.emptyList());
-    this.compactionIOConfigRules = Configs.valueOrDefault(compactionIOConfigRules, Collections.emptyList());
-    this.compactionProjectionRules = Configs.valueOrDefault(compactionProjectionRules, Collections.emptyList());
-    this.compactionGranularityRules = Configs.valueOrDefault(compactionGranularityRules, Collections.emptyList());
-    this.compactionTuningConfigRules = Configs.valueOrDefault(compactionTuningConfigRules, Collections.emptyList());
+    this.reindexingFilterRules = Configs.valueOrDefault(reindexingFilterRules, Collections.emptyList());
+    this.reindexingMetricsRules = Configs.valueOrDefault(reindexingMetricsRules, Collections.emptyList());
+    this.reindexingDimensionsRules = Configs.valueOrDefault(reindexingDimensionsRules, Collections.emptyList());
+    this.reindexingIOConfigRules = Configs.valueOrDefault(reindexingIOConfigRules, Collections.emptyList());
+    this.reindexingProjectionRules = Configs.valueOrDefault(reindexingProjectionRules, Collections.emptyList());
+    this.reindexingGranularityRules = Configs.valueOrDefault(reindexingGranularityRules, Collections.emptyList());
+    this.reindexingTuningConfigRules = Configs.valueOrDefault(reindexingTuningConfigRules, Collections.emptyList());
   }
 
   public static Builder builder()
@@ -130,65 +130,65 @@ public String getType()
   }
 
   @Override
-  @JsonProperty("compactionFilterRules")
+  @JsonProperty("reindexingFilterRules")
   public List getFilterRules()
   {
-    return compactionFilterRules;
+    return reindexingFilterRules;
   }
 
   @Override
-  @JsonProperty("compactionMetricsRules")
+  @JsonProperty("reindexingMetricsRules")
   public List getMetricsRules()
   {
-    return compactionMetricsRules;
+    return reindexingMetricsRules;
   }
 
   @Override
-  @JsonProperty("compactionDimensionsRules")
+  @JsonProperty("reindexingDimensionsRules")
   public List getDimensionsRules()
   {
-    return compactionDimensionsRules;
+    return reindexingDimensionsRules;
   }
 
   @Override
-  @JsonProperty("compactionIOConfigRules")
+  @JsonProperty("reindexingIOConfigRules")
   public List getIOConfigRules()
   {
-    return compactionIOConfigRules;
+    return reindexingIOConfigRules;
   }
 
   @Override
-  @JsonProperty("compactionProjectionRules")
+  @JsonProperty("reindexingProjectionRules")
   public List getProjectionRules()
   {
-    return compactionProjectionRules;
+    return reindexingProjectionRules;
   }
 
   @Override
-  @JsonProperty("compactionGranularityRules")
+  @JsonProperty("reindexingGranularityRules")
   public List getGranularityRules()
   {
-    return compactionGranularityRules;
+    return reindexingGranularityRules;
   }
 
   @Override
-  @JsonProperty("compactionTuningConfigRules")
+  @JsonProperty("reindexingTuningConfigRules")
   public List getTuningConfigRules()
   {
-    return compactionTuningConfigRules;
+    return reindexingTuningConfigRules;
   }
 
   @Override
   public List getCondensedAndSortedPeriods(DateTime referenceTime)
   {
     return Stream.of(
-                     compactionFilterRules,
-                     compactionMetricsRules,
-                     compactionDimensionsRules,
-                     compactionIOConfigRules,
-                     compactionProjectionRules,
-                     compactionGranularityRules,
-                     compactionTuningConfigRules
+                     reindexingFilterRules,
+                     reindexingMetricsRules,
+                     reindexingDimensionsRules,
+                     reindexingIOConfigRules,
+                     reindexingProjectionRules,
+                     reindexingGranularityRules,
+                     reindexingTuningConfigRules
                  )
                  .flatMap(List::stream)
                  .map(ReindexingRule::getPeriod)
@@ -204,43 +204,43 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime)
   @Override
   public List getFilterRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionFilterRules, interval, referenceTime);
+    return getApplicableRules(reindexingFilterRules, interval, referenceTime);
   }
 
   @Override
   public List getMetricsRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionMetricsRules, interval, referenceTime);
+    return getApplicableRules(reindexingMetricsRules, interval, referenceTime);
   }
 
   @Override
   public List getDimensionsRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionDimensionsRules, interval, referenceTime);
+    return getApplicableRules(reindexingDimensionsRules, interval, referenceTime);
   }
 
   @Override
   public List getIOConfigRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionIOConfigRules, interval, referenceTime);
+    return getApplicableRules(reindexingIOConfigRules, interval, referenceTime);
   }
 
   @Override
   public List getProjectionRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionProjectionRules, interval, referenceTime);
+    return getApplicableRules(reindexingProjectionRules, interval, referenceTime);
   }
 
   @Override
   public List getGranularityRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionGranularityRules, interval, referenceTime);
+    return getApplicableRules(reindexingGranularityRules, interval, referenceTime);
   }
 
   @Override
   public List getTuningConfigRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(compactionTuningConfigRules, interval, referenceTime);
+    return getApplicableRules(reindexingTuningConfigRules, interval, referenceTime);
   }
 
   /**
@@ -285,26 +285,26 @@ public boolean equals(Object o)
       return false;
     }
     InlineReindexingRuleProvider that = (InlineReindexingRuleProvider) o;
-    return Objects.equals(compactionFilterRules, that.compactionFilterRules)
-           && Objects.equals(compactionMetricsRules, that.compactionMetricsRules)
-           && Objects.equals(compactionDimensionsRules, that.compactionDimensionsRules)
-           && Objects.equals(compactionIOConfigRules, that.compactionIOConfigRules)
-           && Objects.equals(compactionProjectionRules, that.compactionProjectionRules)
-           && Objects.equals(compactionGranularityRules, that.compactionGranularityRules)
-           && Objects.equals(compactionTuningConfigRules, that.compactionTuningConfigRules);
+    return Objects.equals(reindexingFilterRules, that.reindexingFilterRules)
+           && Objects.equals(reindexingMetricsRules, that.reindexingMetricsRules)
+           && Objects.equals(reindexingDimensionsRules, that.reindexingDimensionsRules)
+           && Objects.equals(reindexingIOConfigRules, that.reindexingIOConfigRules)
+           && Objects.equals(reindexingProjectionRules, that.reindexingProjectionRules)
+           && Objects.equals(reindexingGranularityRules, that.reindexingGranularityRules)
+           && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules);
   }
 
   @Override
   public int hashCode()
   {
     return Objects.hash(
-        compactionFilterRules,
-        compactionMetricsRules,
-        compactionDimensionsRules,
-        compactionIOConfigRules,
-        compactionProjectionRules,
-        compactionGranularityRules,
-        compactionTuningConfigRules
+        reindexingFilterRules,
+        reindexingMetricsRules,
+        reindexingDimensionsRules,
+        reindexingIOConfigRules,
+        reindexingProjectionRules,
+        reindexingGranularityRules,
+        reindexingTuningConfigRules
     );
   }
 
@@ -312,78 +312,78 @@ public int hashCode()
   public String toString()
   {
     return "InlineReindexingRuleProvider{"
-           + "compactionFilterRules=" + compactionFilterRules
-           + ", compactionMetricsRules=" + compactionMetricsRules
-           + ", compactionDimensionsRules=" + compactionDimensionsRules
-           + ", compactionIOConfigRules=" + compactionIOConfigRules
-           + ", compactionProjectionRules=" + compactionProjectionRules
-           + ", compactionGranularityRules=" + compactionGranularityRules
-           + ", compactionTuningConfigRules=" + compactionTuningConfigRules
+           + "reindexingFilterRules=" + reindexingFilterRules
+           + ", reindexingMetricsRules=" + reindexingMetricsRules
+           + ", reindexingDimensionsRules=" + reindexingDimensionsRules
+           + ", reindexingIOConfigRules=" + reindexingIOConfigRules
+           + ", reindexingProjectionRules=" + reindexingProjectionRules
+           + ", reindexingGranularityRules=" + reindexingGranularityRules
+           + ", reindexingTuningConfigRules=" + reindexingTuningConfigRules
            + '}';
   }
 
   public static class Builder
   {
-    private List compactionFilterRules;
-    private List compactionMetricsRules;
-    private List compactionDimensionsRules;
-    private List compactionIOConfigRules;
-    private List compactionProjectionRules;
-    private List compactionGranularityRules;
-    private List compactionTuningConfigRules;
-
-    public Builder filterRules(List compactionFilterRules)
+    private List reindexingFilterRules;
+    private List reindexingMetricsRules;
+    private List reindexingDimensionsRules;
+    private List reindexingIOConfigRules;
+    private List reindexingProjectionRules;
+    private List reindexingGranularityRules;
+    private List reindexingTuningConfigRules;
+
+    public Builder filterRules(List reindexingFilterRules)
     {
-      this.compactionFilterRules = compactionFilterRules;
+      this.reindexingFilterRules = reindexingFilterRules;
       return this;
     }
 
-    public Builder metricsRules(List compactionMetricsRules)
+    public Builder metricsRules(List reindexingMetricsRules)
     {
-      this.compactionMetricsRules = compactionMetricsRules;
+      this.reindexingMetricsRules = reindexingMetricsRules;
       return this;
     }
 
-    public Builder dimensionsRules(List compactionDimensionsRules)
+    public Builder dimensionsRules(List reindexingDimensionsRules)
     {
-      this.compactionDimensionsRules = compactionDimensionsRules;
+      this.reindexingDimensionsRules = reindexingDimensionsRules;
       return this;
     }
 
-    public Builder ioConfigRules(List compactionIOConfigRules)
+    public Builder ioConfigRules(List reindexingIOConfigRules)
     {
-      this.compactionIOConfigRules = compactionIOConfigRules;
+      this.reindexingIOConfigRules = reindexingIOConfigRules;
       return this;
     }
 
-    public Builder projectionRules(List compactionProjectionRules)
+    public Builder projectionRules(List reindexingProjectionRules)
     {
-      this.compactionProjectionRules = compactionProjectionRules;
+      this.reindexingProjectionRules = reindexingProjectionRules;
       return this;
     }
 
-    public Builder granularityRules(List compactionGranularityRules)
+    public Builder granularityRules(List reindexingGranularityRules)
     {
-      this.compactionGranularityRules = compactionGranularityRules;
+      this.reindexingGranularityRules = reindexingGranularityRules;
       return this;
     }
 
-    public Builder tuningConfigRules(List compactionTuningConfigRules)
+    public Builder tuningConfigRules(List reindexingTuningConfigRules)
     {
-      this.compactionTuningConfigRules = compactionTuningConfigRules;
+      this.reindexingTuningConfigRules = reindexingTuningConfigRules;
       return this;
     }
 
     public InlineReindexingRuleProvider build()
     {
       return new InlineReindexingRuleProvider(
-          compactionFilterRules,
-          compactionMetricsRules,
-          compactionDimensionsRules,
-          compactionIOConfigRules,
-          compactionProjectionRules,
-          compactionGranularityRules,
-          compactionTuningConfigRules
+          reindexingFilterRules,
+          reindexingMetricsRules,
+          reindexingDimensionsRules,
+          reindexingIOConfigRules,
+          reindexingProjectionRules,
+          reindexingGranularityRules,
+          reindexingTuningConfigRules
       );
     }
   }

From 3108b61fd6cddcb0e6a1c6209e6c6af094c8cca6 Mon Sep 17 00:00:00 2001
From: capistrant 
Date: Thu, 22 Jan 2026 12:23:55 -0600
Subject: [PATCH 14/90] fix up a javadoc with up to date design spec

---
 .../compact/CascadingReindexingTemplate.java  | 26 ++++++++++++-------
 1 file changed, 16 insertions(+), 10 deletions(-)

diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java
index 4495642e9d35..a706f191b59f 100644
--- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java
+++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java
@@ -61,19 +61,25 @@
 import java.util.stream.Collectors;
 
 /**
- * Template to perform period-based cascading reindexing. Contains a list of
- * {@link ReindexingRule} which divide the segment timeline into reindexable
- * intervals. Each rule specifies a period relative to the current time which is
- * used to determine its applicable interval:
+ * Template to perform period-based cascading reindexing. {@link ReindexingRule} are provided by a {@link ReindexingRuleProvider}
+ * Each rule specifies a period relative to the current time which is used to determine its applicable interval.
+ * A timeline is constructed from a condensed set of these periods and tasks are created for each search interval in
+ * the timeline with the applicable rules for said interval.
+ * 

+ * For example if you had the following rules: *

    - *
  • Rule 1: range = [now - p1, +inf)
  • - *
  • Rule 2: range = [now - p2, now - p1)
  • - *
  • ...
  • - *
  • Rule n: range = (-inf, now - p(n - 1))
  • + *
  • Rule A: period = 1 day
  • + *
  • Rule B: period = 7 days
  • + *
  • Rule C: period = 30 days
  • + *
  • Rule D: period = 7 days
  • *
* - * If two adjacent rules explicitly specify a segment granularity, the boundary - * between them may be adjusted to ensure that there are no unprocessed gaps in the timeline. + * You would end up with the following search intervals (assuming current time is T): + *
    + *
  • Interval 2: [T-7days, T-1day)
  • + *
  • Interval 3: [T-30days, T-7days)
  • + *
  • Interval 3: [-inf, T-30days)
  • + *
*

* This template never needs to be deserialized as a {@code BatchIndexingJobTemplate} */ From 1f671e154fce44815b9a684ec56fbf3c0d2808b8 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 14:31:26 -0600 Subject: [PATCH 15/90] Fill in UT gaps for the composing provider --- .../ComposingReindexingRuleProviderTest.java | 546 +++++++++++------- 1 file changed, 340 insertions(+), 206 deletions(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 8ff4da8c8c80..2002b47a26bc 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -20,11 +20,17 @@ package org.apache.druid.server.compaction; import com.google.common.collect.ImmutableList; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; 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.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -34,6 +40,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; public class ComposingReindexingRuleProviderTest { @@ -52,7 +60,7 @@ public void test_constructor_nullProviders_throwsNullPointerException() public void test_constructor_nullProviderInList_throwsNullPointerException() { List providers = new ArrayList<>(); - providers.add(createEmptyInlineProvider()); + providers.add(InlineReindexingRuleProvider.builder().build()); providers.add(null); // Null provider NullPointerException exception = Assert.assertThrows( @@ -75,21 +83,12 @@ public void test_constructor_emptyProviderList_succeeds() Assert.assertTrue(composing.getFilterRules().isEmpty()); } - @Test - public void test_getType_returnsComposing() - { - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(createEmptyInlineProvider()) - ); - - Assert.assertEquals("composing", composing.getType()); - } @Test public void test_isReady_allProvidersReady_returnsTrue() { - ReindexingRuleProvider provider1 = createEmptyInlineProvider(); // Always ready - ReindexingRuleProvider provider2 = createEmptyInlineProvider(); // Always ready + ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder().build(); + ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder().build(); ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) @@ -101,7 +100,7 @@ public void test_isReady_allProvidersReady_returnsTrue() @Test public void test_isReady_someProvidersNotReady_returnsFalse() { - ReindexingRuleProvider readyProvider = createEmptyInlineProvider(); + ReindexingRuleProvider readyProvider = InlineReindexingRuleProvider.builder().build(); ReindexingRuleProvider notReadyProvider = createNotReadyProvider(); ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( @@ -122,278 +121,352 @@ public void test_isReady_emptyProviderList_returnsTrue() } @Test - public void test_getFilterRules_firstWins_returnsFirstNonEmpty() + public void test_getFilterRules_compositingBehavior() { - ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(30)); - ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(60)); - - ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); - - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider1, provider2) + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().filterRules(rules).build(), + ComposingReindexingRuleProvider::getFilterRules, + createFilterRule("rule1", Period.days(7)), + createFilterRule("rule2", Period.days(30)), + ReindexingFilterRule::getId ); - - List result = composing.getFilterRules(); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", result.get(0).getId()); } @Test - public void test_getFilterRules_firstProviderEmpty_returnsSecond() + public void test_getFilterRulesWithInterval_compositingBehavior() { - ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(60)); - - ReindexingRuleProvider emptyProvider = createEmptyInlineProvider(); - ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2)); - - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(emptyProvider, provider2) + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().filterRules(rules).build(), + (provider, it) -> provider.getFilterRules(it.interval, it.time), + createFilterRule("rule1", Period.days(7)), + createFilterRule("rule2", Period.days(30)), + ReindexingFilterRule::getId ); - - List result = composing.getFilterRules(); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule2", result.get(0).getId()); } @Test - public void test_getFilterRules_allProvidersEmpty_returnsEmpty() + public void test_getGranularityRules_compositingBehavior() { - ReindexingRuleProvider provider1 = createEmptyInlineProvider(); - ReindexingRuleProvider provider2 = createEmptyInlineProvider(); - - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider1, provider2) + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().granularityRules(rules).build(), + ComposingReindexingRuleProvider::getGranularityRules, + createGranularityRule("rule1", Period.days(7)), + createGranularityRule("rule2", Period.days(30)), + ReindexingGranularityRule::getId ); - - List result = composing.getFilterRules(); - - Assert.assertTrue(result.isEmpty()); } @Test - public void test_getGranularityRules_firstWins_returnsFirstNonEmpty() + public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() { - ReindexingGranularityRule rule1 = createGranularityRule("rule1", Period.days(7)); - ReindexingGranularityRule rule2 = createGranularityRule("rule2", Period.days(30)); + ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(7)); + ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(30)); + ReindexingFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period - ReindexingRuleProvider provider1 = createInlineProviderWithGranularityRules(ImmutableList.of(rule1)); - ReindexingRuleProvider provider2 = createInlineProviderWithGranularityRules(ImmutableList.of(rule2)); + ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(rule1)).build(); + ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(rule2, rule3)).build(); ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) ); - List result = composing.getGranularityRules(); + List result = composing.getCondensedAndSortedPeriods(REFERENCE_TIME); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", result.get(0).getId()); + // Should be deduplicated and sorted: [P7D, P30D] + Assert.assertEquals(2, result.size()); + Assert.assertEquals(Period.days(7), result.get(0)); + Assert.assertEquals(Period.days(30), result.get(1)); } + @Test - public void test_getFilterRulesWithInterval_firstWins_delegatesToFirstProvider() + public void test_getMetricsRules_compositingBehavior() { - Interval interval = Intervals.of("2025-11-01T00:00:00Z/2025-11-15T00:00:00Z"); - ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(30)); - - ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - ReindexingRuleProvider provider2 = createEmptyInlineProvider(); - - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider1, provider2) + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().metricsRules(rules).build(), + ComposingReindexingRuleProvider::getMetricsRules, + createMetricsRule("rule1", Period.days(7)), + createMetricsRule("rule2", Period.days(30)), + ReindexingMetricsRule::getId ); - - List result = composing.getFilterRules(interval, REFERENCE_TIME); - - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", result.get(0).getId()); } @Test - public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() + public void test_getMetricsRulesWithInterval_compositingBehavior() { - ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(7)); - ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(30)); - ReindexingFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period - - ReindexingRuleProvider provider1 = createInlineProviderWithFilterRules(ImmutableList.of(rule1)); - ReindexingRuleProvider provider2 = createInlineProviderWithFilterRules(ImmutableList.of(rule2, rule3)); + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().metricsRules(rules).build(), + (provider, it) -> provider.getMetricsRules(it.interval, it.time), + createMetricsRule("rule1", Period.days(7)), + createMetricsRule("rule2", Period.days(30)), + ReindexingMetricsRule::getId + ); + } - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider1, provider2) + @Test + public void test_getDimensionsRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().dimensionsRules(rules).build(), + ComposingReindexingRuleProvider::getDimensionsRules, + createDimensionsRule("rule1", Period.days(7)), + createDimensionsRule("rule2", Period.days(30)), + ReindexingDimensionsRule::getId ); + } - List result = composing.getCondensedAndSortedPeriods(REFERENCE_TIME); + @Test + public void test_getDimensionsRulesWithInterval_compositingBehavior() + { + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().dimensionsRules(rules).build(), + (provider, it) -> provider.getDimensionsRules(it.interval, it.time), + createDimensionsRule("rule1", Period.days(7)), + createDimensionsRule("rule2", Period.days(30)), + ReindexingDimensionsRule::getId + ); + } - // Should be deduplicated and sorted: [P7D, P30D] - Assert.assertEquals(2, result.size()); - Assert.assertEquals(Period.days(7), result.get(0)); - Assert.assertEquals(Period.days(30), result.get(1)); + @Test + public void test_getIOConfigRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().ioConfigRules(rules).build(), + ComposingReindexingRuleProvider::getIOConfigRules, + createIOConfigRule("rule1", Period.days(7)), + createIOConfigRule("rule2", Period.days(30)), + ReindexingIOConfigRule::getId + ); } @Test - public void test_singleProvider_delegatesDirectly() + public void test_getIOConfigRulesWithInterval_compositingBehavior() { - ReindexingFilterRule rule = createFilterRule("rule1", Period.days(30)); - ReindexingRuleProvider provider = createInlineProviderWithFilterRules(ImmutableList.of(rule)); + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().ioConfigRules(rules).build(), + (provider, it) -> provider.getIOConfigRules(it.interval, it.time), + createIOConfigRule("rule1", Period.days(7)), + createIOConfigRule("rule2", Period.days(30)), + ReindexingIOConfigRule::getId + ); + } - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider) + @Test + public void test_getProjectionRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), + ComposingReindexingRuleProvider::getProjectionRules, + createProjectionRule("rule1", Period.days(7)), + createProjectionRule("rule2", Period.days(30)), + ReindexingProjectionRule::getId ); + } - List result = composing.getFilterRules(); + @Test + public void test_getProjectionRulesWithInterval_compositingBehavior() + { + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), + (provider, it) -> provider.getProjectionRules(it.interval, it.time), + createProjectionRule("rule1", Period.days(7)), + createProjectionRule("rule2", Period.days(30)), + ReindexingProjectionRule::getId + ); + } - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", result.get(0).getId()); + @Test + public void test_getTuningConfigRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().tuningConfigRules(rules).build(), + ComposingReindexingRuleProvider::getTuningConfigRules, + createTuningConfigRule("rule1", Period.days(7)), + createTuningConfigRule("rule2", Period.days(30)), + ReindexingTuningConfigRule::getId + ); } - // ========== Helper Methods ========== + @Test + public void test_getTuningConfigRulesWithInterval_compositingBehavior() + { + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().tuningConfigRules(rules).build(), + (provider, it) -> provider.getTuningConfigRules(it.interval, it.time), + createTuningConfigRule("rule1", Period.days(7)), + createTuningConfigRule("rule2", Period.days(30)), + ReindexingTuningConfigRule::getId + ); + } - private ReindexingRuleProvider createEmptyInlineProvider() + @Test + public void test_getGranularityRulesWithInterval_compositingBehavior() { - return new InlineReindexingRuleProvider( - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList() + testComposingBehaviorForRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().granularityRules(rules).build(), + (provider, it) -> provider.getGranularityRules(it.interval, it.time), + createGranularityRule("rule1", Period.days(7)), + createGranularityRule("rule2", Period.days(30)), + ReindexingGranularityRule::getId ); } - private ReindexingRuleProvider createInlineProviderWithFilterRules(List rules) + + @Test + public void test_equals_sameProviders_returnsTrue() { - return new InlineReindexingRuleProvider( - rules, - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList() + ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder().build(); + ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) + .build(); + + ComposingReindexingRuleProvider composing1 = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) + ); + ComposingReindexingRuleProvider composing2 = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) ); + + Assert.assertEquals(composing1, composing2); + Assert.assertEquals(composing1.hashCode(), composing2.hashCode()); } - private ReindexingRuleProvider createInlineProviderWithGranularityRules(List rules) + @Test + public void test_equals_differentProviders_returnsFalse() { - return new InlineReindexingRuleProvider( - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), - rules, - Collections.emptyList() + ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder().build(); + ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) + .build(); + + ComposingReindexingRuleProvider composing1 = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1) ); + ComposingReindexingRuleProvider composing2 = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + Assert.assertNotEquals(composing1, composing2); } - private ReindexingRuleProvider createNotReadyProvider() + + + /** + * Helper class to pass interval + time together + */ + private static class IntervalAndTime { - return new ReindexingRuleProvider() + final Interval interval; + final DateTime time; + + IntervalAndTime(Interval interval, DateTime time) { - @Override - public String getType() - { - return "not-ready-test-provider"; - } + this.interval = interval; + this.time = time; + } + } - @Override - public boolean isReady() - { - return false; - } + /** + * Tests composing behavior for getXxxRules() - all three scenarios: + * 1. First provider has rules → returns first provider's rules + * 2. First provider empty → falls through to second provider + * 3. Both providers empty → returns empty list + */ + private void testComposingBehaviorForRuleType( + Function, ReindexingRuleProvider> providerFactory, + Function> ruleGetter, + T rule1, + T rule2, + Function idExtractor + ) + { + ReindexingRuleProvider provider1 = providerFactory.apply(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = providerFactory.apply(ImmutableList.of(rule2)); - @Override - public List getCondensedAndSortedPeriods(DateTime referenceTime) - { - return Collections.emptyList(); - } + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) + ); - @Override - public List getFilterRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + List result = ruleGetter.apply(composing); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", idExtractor.apply(result.get(0))); - @Override - public List getFilterRules() - { - return Collections.emptyList(); - } + ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, provider2) + ); - @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + result = ruleGetter.apply(composing); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule2", idExtractor.apply(result.get(0))); - @Override - public List getMetricsRules() - { - return Collections.emptyList(); - } + ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, emptyProvider2) + ); - @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + result = ruleGetter.apply(composing); + Assert.assertTrue(result.isEmpty()); + } - @Override - public List getDimensionsRules() - { - return Collections.emptyList(); - } + /** + * Tests composing behavior for getXxxRules(interval, time) - all three scenarios: + * 1. First provider has rules → returns first provider's rules + * 2. First provider empty → falls through to second provider + * 3. Both providers empty → returns empty list + */ + private void testComposingBehaviorForRuleTypeWithInterval( + Function, ReindexingRuleProvider> providerFactory, + BiFunction> ruleGetter, + T rule1, + T rule2, + Function idExtractor + ) + { + Interval interval = Intervals.of("2025-11-01/2025-11-15"); - @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + ReindexingRuleProvider provider1 = providerFactory.apply(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = providerFactory.apply(ImmutableList.of(rule2)); - @Override - public List getIOConfigRules() - { - return Collections.emptyList(); - } + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) + ); - @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + List result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule1", idExtractor.apply(result.get(0))); - @Override - public List getProjectionRules() - { - return Collections.emptyList(); - } + ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, provider2) + ); - @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("rule2", idExtractor.apply(result.get(0))); - @Override - public List getGranularityRules() - { - return Collections.emptyList(); - } + ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, emptyProvider2) + ); - @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) - { - return Collections.emptyList(); - } + result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertTrue(result.isEmpty()); + } + /** + * Creates a test provider that is not ready + */ + private ReindexingRuleProvider createNotReadyProvider() + { + return new InlineReindexingRuleProvider(null, null, null, null, null, null, null) + { @Override - public List getTuningConfigRules() + public boolean isReady() { - return Collections.emptyList(); + return false; } }; } @@ -417,4 +490,65 @@ private ReindexingGranularityRule createGranularityRule(String id, Period period new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) ); } + + private ReindexingMetricsRule createMetricsRule(String id, Period period) + { + return new ReindexingMetricsRule( + id, + "Test metrics rule", + period, + new AggregatorFactory[]{new CountAggregatorFactory("count")} + ); + } + + private ReindexingDimensionsRule createDimensionsRule(String id, Period period) + { + return new ReindexingDimensionsRule( + id, + "Test dimensions rule", + period, + new UserCompactionTaskDimensionsConfig(null) + ); + } + + private ReindexingIOConfigRule createIOConfigRule(String id, Period period) + { + return new ReindexingIOConfigRule( + id, + "Test IO config rule", + period, + new UserCompactionTaskIOConfig(null) + ); + } + + private ReindexingProjectionRule createProjectionRule(String id, Period period) + { + AggregateProjectionSpec projectionSpec = new AggregateProjectionSpec( + "test_projection", + null, + null, + null, + new AggregatorFactory[]{new CountAggregatorFactory("count")} + ); + return new ReindexingProjectionRule( + id, + "Test projection rule", + period, + ImmutableList.of(projectionSpec) + ); + } + + private ReindexingTuningConfigRule createTuningConfigRule(String id, Period period) + { + return new ReindexingTuningConfigRule( + id, + "Test tuning config rule", + period, + new UserCompactionTaskQueryTuningConfig( + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null + ) + ); + } + } From 3be0da13771a6d29045ea89beca24a665d06405c Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 14:44:13 -0600 Subject: [PATCH 16/90] refactor test class for inline rule provider --- .../ComposingReindexingRuleProviderTest.java | 3 +- .../InlineReindexingRuleProviderTest.java | 362 ++++++++---------- 2 files changed, 161 insertions(+), 204 deletions(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 2002b47a26bc..499c06f82b62 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -544,8 +544,7 @@ private ReindexingTuningConfigRule createTuningConfigRule(String id, Period peri id, "Test tuning config rule", period, - new UserCompactionTaskQueryTuningConfig( - null, null, null, null, null, null, null, null, + new UserCompactionTaskQueryTuningConfig(null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ) ); diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index c3b1c0dd20ce..e4491e3341b1 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -19,25 +19,30 @@ package org.apache.druid.server.compaction; +import com.google.common.collect.ImmutableList; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; 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.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; import org.junit.Test; -import java.util.Collections; import java.util.List; public class InlineReindexingRuleProviderTest { private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); - // Test intervals private static final Interval INTERVAL_100_DAYS_OLD = Intervals.of( "2025-09-01T00:00:00Z/2025-09-02T00:00:00Z" ); // Ends 109 days before reference time @@ -50,261 +55,214 @@ public class InlineReindexingRuleProviderTest "2025-11-20T00:00:00Z/2025-11-21T00:00:00Z" ); // Ends 28 days before reference time - private static final Interval INTERVAL_5_DAYS_OLD = Intervals.of( - "2025-12-13T00:00:00Z/2025-12-14T00:00:00Z" - ); // Ends 5 days before reference time - @Test - public void test_getFilterRules_noRulesMatch_returnsEmpty() + public void test_constructor_nullListsDefaultToEmpty() { - ReindexingFilterRule rule30d = new ReindexingFilterRule( - "filter-30d", - null, - Period.days(30), - new SelectorDimFilter("dim", "val", null) - ); + InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider(null, null, null, null, + null, null, null); - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d)).build(); - - // Interval is only 5 days old, rule requires 30 days - List result = provider.getFilterRules(INTERVAL_5_DAYS_OLD, REFERENCE_TIME); - - Assert.assertTrue("Should return empty when no rules match", result.isEmpty()); + Assert.assertNotNull(provider.getFilterRules()); + Assert.assertTrue(provider.getFilterRules().isEmpty()); + Assert.assertNotNull(provider.getMetricsRules()); + Assert.assertTrue(provider.getMetricsRules().isEmpty()); + Assert.assertNotNull(provider.getDimensionsRules()); + Assert.assertTrue(provider.getDimensionsRules().isEmpty()); + Assert.assertNotNull(provider.getIOConfigRules()); + Assert.assertTrue(provider.getIOConfigRules().isEmpty()); + Assert.assertNotNull(provider.getProjectionRules()); + Assert.assertTrue(provider.getProjectionRules().isEmpty()); + Assert.assertNotNull(provider.getGranularityRules()); + Assert.assertTrue(provider.getGranularityRules().isEmpty()); + Assert.assertNotNull(provider.getTuningConfigRules()); + Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); } @Test - public void test_getFilterRules_oneRuleMatchesFull_returnsThatRule() + public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() { - ReindexingFilterRule rule30d = new ReindexingFilterRule( - "filter-30d", - null, - Period.days(30), - new SelectorDimFilter("dim", "val", null) - ); + ReindexingFilterRule filter30d = createFilterRule("f1", Period.days(30)); + ReindexingFilterRule filter60d = createFilterRule("f2", Period.days(60)); + ReindexingGranularityRule gran30d = createGranularityRule("g1", Period.days(30)); // Duplicate P30D + ReindexingGranularityRule gran90d = createGranularityRule("g2", Period.days(90)); - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(filter30d, filter60d)) + .granularityRules(ImmutableList.of(gran30d, gran90d)) + .build(); - // Interval is 50 days old, rule requires 30 days - FULL match - List result = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("filter-30d", result.get(0).getId()); + Assert.assertEquals(3, periods.size()); + + Assert.assertEquals(Period.days(30), periods.get(0)); + Assert.assertEquals(Period.days(60), periods.get(1)); + Assert.assertEquals(Period.days(90), periods.get(2)); } @Test - public void test_getFilterRules_multipleAdditiveRulesMatchFull_returnsAll() + public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() { - // Filter rules are additive - should return all matching rules - ReindexingFilterRule rule30d = new ReindexingFilterRule( - "filter-30d", - null, - Period.days(30), - new SelectorDimFilter("dim1", "val1", null) - ); - - ReindexingFilterRule rule60d = new ReindexingFilterRule( - "filter-60d", - null, - Period.days(60), - new SelectorDimFilter("dim2", "val2", null) - ); - - ReindexingFilterRule rule90d = new ReindexingFilterRule( - "filter-90d", - null, - Period.days(90), - new SelectorDimFilter("dim3", "val3", null) - ); - - - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(rule30d, rule60d, rule90d)).build(); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); - // Interval is 100 days old - all three rules match - List result = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); - Assert.assertEquals("Should return all matching additive rules", 3, result.size()); - Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-30d"))); - Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-60d"))); - Assert.assertTrue(result.stream().anyMatch(r -> r.getId().equals("filter-90d"))); + Assert.assertTrue(periods.isEmpty()); } @Test - public void test_getGranularityRules_multipleNonAdditiveRulesMatchFull_returnsOldestThreshold() + public void test_additiveRules_allScenarios() { - // Granularity rules are NOT additive - should return only the one with oldest threshold - ReindexingGranularityRule rule30d = new ReindexingGranularityRule( - "gran-30d", - null, - Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ); - - ReindexingGranularityRule rule60d = new ReindexingGranularityRule( - "gran-60d", - null, - Period.days(60), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ); - - ReindexingGranularityRule rule90d = new ReindexingGranularityRule( - "gran-90d", - null, - Period.days(90), - new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) - ); - - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule30d, rule60d, rule90d)).build(); - - // Interval is 100 days old - all three rules match FULL - // Should return rule90d because it has the oldest threshold (now - 90d) - List result = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); - - Assert.assertEquals("Should return only one non-additive rule", 1, result.size()); - Assert.assertEquals("gran-90d", result.get(0).getId()); - Assert.assertEquals(Granularities.MONTH, result.get(0).getGranularityConfig().getSegmentGranularity()); + ReindexingFilterRule rule30d = createFilterRule("filter-30d", Period.days(30)); + ReindexingFilterRule rule60d = createFilterRule("filter-60d", Period.days(60)); + ReindexingFilterRule rule90d = createFilterRule("filter-90d", Period.days(90)); + + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(rule30d, rule60d, rule90d)) + .build(); + + List noMatch = provider.getFilterRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + Assert.assertTrue("No rules should match interval that's too recent", noMatch.isEmpty()); + + List oneMatch = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("Only rule30d should match", 1, oneMatch.size()); + Assert.assertEquals("filter-30d", oneMatch.get(0).getId()); + + List multiMatch = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("All 3 additive rules should be returned", 3, multiMatch.size()); + Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-30d"))); + Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-60d"))); + Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-90d"))); } @Test - public void test_getGranularityRules_partialMatchNotReturned() + public void test_nonAdditiveRules_allScenarios() { - ReindexingGranularityRule rule30d = new ReindexingGranularityRule( - "gran-30d", - null, - Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ); + ReindexingGranularityRule rule30d = createGranularityRule("gran-30d", Period.days(30)); + ReindexingGranularityRule rule60d = createGranularityRule("gran-60d", Period.days(60)); + ReindexingGranularityRule rule90d = createGranularityRule("gran-90d", Period.days(90)); + + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .granularityRules(ImmutableList.of(rule30d, rule60d, rule90d)) + .build(); - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule30d)).build(); + List noMatch = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + Assert.assertTrue("No rules should match interval that's too recent", noMatch.isEmpty()); - // Interval is 20 days old, but rule requires 30 days - // The interval likely has PARTIAL or NONE match, not FULL - List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + List oneMatch = provider.getGranularityRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("Only rule30d should match", 1, oneMatch.size()); + Assert.assertEquals("gran-30d", oneMatch.get(0).getId()); - Assert.assertTrue("Should not return rules with PARTIAL match", result.isEmpty()); + List multiMatch = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("Only one non-additive rule should be returned", 1, multiMatch.size()); + Assert.assertEquals("Should return rule with oldest threshold (P90D)", "gran-90d", multiMatch.get(0).getId()); } @Test - public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() + public void test_allRuleTypesWireCorrectly_withInterval() { - ReindexingFilterRule filter30d = new ReindexingFilterRule( - "f1", null, Period.days(30), new SelectorDimFilter("d", "v", null) - ); - ReindexingFilterRule filter60d = new ReindexingFilterRule( - "f2", null, Period.days(60), new SelectorDimFilter("d", "v", null) - ); - ReindexingGranularityRule gran30d = new ReindexingGranularityRule( - "g1", null, Period.days(30), new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ); - ReindexingGranularityRule gran90d = new ReindexingGranularityRule( - "g2", null, Period.days(90), new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ); - - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(List.of(filter30d, filter60d)).granularityRules(List.of(gran30d, gran90d)).build(); - - List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); - - // Should have 3 distinct periods: P30D (appears twice), P60D, P90D - Assert.assertEquals(3, periods.size()); - // Should be sorted by duration (ascending) - Assert.assertEquals(Period.days(30), periods.get(0)); - Assert.assertEquals(Period.days(60), periods.get(1)); - Assert.assertEquals(Period.days(90), periods.get(2)); + ReindexingFilterRule filterRule = createFilterRule("filter", Period.days(30)); + ReindexingMetricsRule metricsRule = createMetricsRule("metrics", Period.days(30)); + ReindexingDimensionsRule dimensionsRule = createDimensionsRule("dimensions", Period.days(30)); + ReindexingIOConfigRule ioConfigRule = createIOConfigRule("ioconfig", Period.days(30)); + ReindexingProjectionRule projectionRule = createProjectionRule("projection", Period.days(30)); + ReindexingGranularityRule granularityRule = createGranularityRule("granularity", Period.days(30)); + ReindexingTuningConfigRule tuningConfigRule = createTuningConfigRule("tuning", Period.days(30)); + + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .filterRules(ImmutableList.of(filterRule)) + .metricsRules(ImmutableList.of(metricsRule)) + .dimensionsRules(ImmutableList.of(dimensionsRule)) + .ioConfigRules(ImmutableList.of(ioConfigRule)) + .projectionRules(ImmutableList.of(projectionRule)) + .granularityRules(ImmutableList.of(granularityRule)) + .tuningConfigRules(ImmutableList.of(tuningConfigRule)) + .build(); + + Assert.assertEquals(1, provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("filter", provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getMetricsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("metrics", provider.getMetricsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getDimensionsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("dimensions", provider.getDimensionsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getIOConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("ioconfig", provider.getIOConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("projection", provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("granularity", provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + + Assert.assertEquals(1, provider.getTuningConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("tuning", provider.getTuningConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); } @Test - public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() + public void test_getType_returnsInline() { - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().filterRules(Collections.emptyList()).build(); - - List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); + InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); + Assert.assertEquals("inline", provider.getType()); + } - Assert.assertTrue(periods.isEmpty()); + private ReindexingFilterRule createFilterRule(String id, Period period) + { + return new ReindexingFilterRule(id, null, period, new SelectorDimFilter("dim", "val", null)); } - @Test - public void test_getProjectionRules_multipleAdditiveRulesMatchFull_returnsAll() + private ReindexingMetricsRule createMetricsRule(String id, Period period) { - // Projection rules are additive - ReindexingProjectionRule proj30d = new ReindexingProjectionRule( - "proj-30d", null, Period.days(30), Collections.emptyList() - ); - ReindexingProjectionRule proj60d = new ReindexingProjectionRule( - "proj-60d", null, Period.days(60), Collections.emptyList() + return new ReindexingMetricsRule( + id, + null, + period, + new AggregatorFactory[]{new CountAggregatorFactory("count")} ); + } - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().projectionRules(List.of(proj30d, proj60d)).build(); - - // Interval is 100 days old - both rules match - List result = provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + private ReindexingDimensionsRule createDimensionsRule(String id, Period period) + { + return new ReindexingDimensionsRule(id, null, period, new UserCompactionTaskDimensionsConfig(null)); + } - Assert.assertEquals("Should return all matching additive projection rules", 2, result.size()); + private ReindexingIOConfigRule createIOConfigRule(String id, Period period) + { + return new ReindexingIOConfigRule(id, null, period, new UserCompactionTaskIOConfig(null)); } - @Test - public void test_getApplicableRules_mixOfFullPartialNone_onlyReturnsFull() + private ReindexingProjectionRule createProjectionRule(String id, Period period) { - // Create rules that will have different AppliesToMode results - ReindexingGranularityRule rule10d = new ReindexingGranularityRule( - "gran-10d", + AggregateProjectionSpec projectionSpec = new AggregateProjectionSpec( + "test_projection", null, - Period.days(10), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) - ); // Will match FULL for 20-day-old interval - - ReindexingGranularityRule rule25d = new ReindexingGranularityRule( - "gran-25d", null, - Period.days(25), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) - ); // Will match FULL for 20-day-old interval - - ReindexingGranularityRule rule50d = new ReindexingGranularityRule( - "gran-50d", null, - Period.days(50), - new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) - ); // Will be NONE for 20-day-old interval - - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().granularityRules(List.of(rule10d, rule25d, rule50d)).build(); - - // Interval is 20 days old (ends at 2025-11-21, reference is 2025-12-19) - // rule10d: threshold = now - 10d = 2025-12-09, interval ends 2025-11-21 < 2025-12-09 -> FULL - // rule25d: threshold = now - 25d = 2025-11-24, interval ends 2025-11-21 < 2025-11-24 -> FULL - // rule50d: threshold = now - 50d = 2025-10-30, interval ends 2025-11-21 > 2025-10-30 -> NONE - // When multiple rules match, select the one with oldest threshold (smallest millis) = rule25d - List result = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); - - // Should return rule25d (has oldest threshold among FULL matches) - Assert.assertEquals(1, result.size()); - Assert.assertEquals("gran-25d", result.get(0).getId()); + new AggregatorFactory[]{new CountAggregatorFactory("count")} + ); + return new ReindexingProjectionRule(id, null, period, ImmutableList.of(projectionSpec)); } - @Test - public void test_constructor_nullListsDefaultToEmpty() + private ReindexingGranularityRule createGranularityRule(String id, Period period) { - InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider( + return new ReindexingGranularityRule( + id, null, - null, - null, - null, - null, - null, - null + period, + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) ); - - Assert.assertNotNull(provider.getFilterRules()); - Assert.assertTrue(provider.getFilterRules().isEmpty()); - Assert.assertNotNull(provider.getMetricsRules()); - Assert.assertTrue(provider.getMetricsRules().isEmpty()); - Assert.assertNotNull(provider.getGranularityRules()); - Assert.assertTrue(provider.getGranularityRules().isEmpty()); } - @Test - public void test_getType_returnsInline() + private ReindexingTuningConfigRule createTuningConfigRule(String id, Period period) { - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); - - Assert.assertEquals("inline", provider.getType()); + return new ReindexingTuningConfigRule( + id, + null, + period, + new UserCompactionTaskQueryTuningConfig(null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null + ) + ); } } From 79ff44be6da30004af10553bef99e7575fee0241 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 16:18:59 -0600 Subject: [PATCH 17/90] Self review refactorings --- .../compact/CascadingReindexingTemplate.java | 46 +++++++++++++------ .../compaction/AbstractReindexingRule.java | 30 ++++++------ .../ComposingReindexingRuleProvider.java | 16 ++++--- .../InlineReindexingRuleProvider.java | 6 +-- .../server/compaction/ReindexingRule.java | 2 +- .../compaction/ReindexingRuleProvider.java | 2 + .../ComposingReindexingRuleProviderTest.java | 7 ++- 7 files changed, 63 insertions(+), 46 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index a706f191b59f..c01a6f3d5272 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -32,6 +32,7 @@ import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.compaction.ReindexingDimensionsRule; import org.apache.druid.server.compaction.ReindexingFilterRule; @@ -76,8 +77,8 @@ * * You would end up with the following search intervals (assuming current time is T): *

    - *
  • Interval 2: [T-7days, T-1day)
  • - *
  • Interval 3: [T-30days, T-7days)
  • + *
  • Interval 1: [T-7days, T-1day)
  • + *
  • Interval 2: [T-30days, T-7days)
  • *
  • Interval 3: [-inf, T-30days)
  • *
*

@@ -112,12 +113,8 @@ public CascadingReindexingTemplate( this.ruleProvider = Objects.requireNonNull(ruleProvider, "'ruleProvider' cannot be null"); this.engine = engine; this.taskContext = taskContext; - this.taskPriority = taskPriority == null - ? DEFAULT_COMPACTION_TASK_PRIORITY - : taskPriority; - this.inputSegmentSizeBytes = inputSegmentSizeBytes == null - ? DEFAULT_INPUT_SEGMENT_SIZE_BYTES - : inputSegmentSizeBytes; + this.taskPriority = Objects.requireNonNullElse(taskPriority, DEFAULT_COMPACTION_TASK_PRIORITY); + this.inputSegmentSizeBytes = Objects.requireNonNullElse(inputSegmentSizeBytes, DEFAULT_INPUT_SEGMENT_SIZE_BYTES); } @Override @@ -179,11 +176,7 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() { return (config, candidate, params) -> { // Only optimize if candidate has been reindexed before and config has a NotDimFilter - if (candidate.getCurrentStatus() != null && - !candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON) && - config.getTransformSpec() != null && - config.getTransformSpec().getFilter() != null && - config.getTransformSpec().getFilter() instanceof NotDimFilter) { + if (shouldOptimizeFilterRules(candidate, config)) { // Compute the minimal set of filter rules needed for this candidate NotDimFilter reducedTransformSpecFilter = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( @@ -202,6 +195,31 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() }; } + /** + * Determines if we should optimize filter rules for this candidate. + * Returns true only if the candidate has been compacted before and has a NotDimFilter. + */ + private static boolean shouldOptimizeFilterRules( + CompactionCandidate candidate, + DataSourceCompactionConfig config + ) + { + if (candidate.getCurrentStatus() == null) { + return false; + } + + if (candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON)) { + return false; + } + + if (config.getTransformSpec() == null) { + return false; + } + + DimFilter filter = config.getTransformSpec().getFilter(); + return filter instanceof NotDimFilter; + } + @Override public List createCompactionJobs( DruidInputSource source, @@ -222,7 +240,7 @@ public List createCompactionJobs( final DateTime currentTime = jobParams.getScheduleStartTime(); List sortedPeriods = ruleProvider.getCondensedAndSortedPeriods(currentTime); - if (sortedPeriods == null || sortedPeriods.isEmpty()) { + if (sortedPeriods.isEmpty()) { return Collections.emptyList(); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index a6321cc62149..b9521dec9467 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -74,19 +74,13 @@ public AbstractReindexingRule( */ private static void validatePeriodIsPositive(Period period) { - try { - // Try converting to standard duration for precise validation - if (period.toStandardDuration().getMillis() <= 0) { - throw new IllegalArgumentException("period must be positive, got: " + period); - } - } - catch (UnsupportedOperationException e) { - // Period contains months or years which have variable length - // Validate that at least one component is positive + if (hasMonthsOrYears(period)) { if (!isPeriodPositive(period)) { - throw new IllegalArgumentException( - "period must be positive (contains months/years but all components are non-positive), got: " + period - ); + throw new IllegalArgumentException("period must be positive. Supplied period: " + period); + } + } else { + if (period.toStandardDuration().getMillis() <= 0) { + throw new IllegalArgumentException("period must be positive. Supplied period: " + period); } } } @@ -133,14 +127,10 @@ public Period getPeriod() @Override public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTime) { - DateTime now = DateTimes.nowUtc(); + DateTime now = (referenceTime != null) ? referenceTime : DateTimes.nowUtc(); DateTime intervalEnd = interval.getEnd(); DateTime intervalStart = interval.getStart(); - if (referenceTime != null) { - // Use the provided reference time instead of actual "now" for checking rule applicability - now = referenceTime; - } DateTime threshold = now.minus(period); if (intervalEnd.isBefore(threshold) || intervalEnd.isEqual(threshold)) { @@ -154,4 +144,10 @@ public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTi return AppliesToMode.PARTIAL; } } + + private static boolean hasMonthsOrYears(Period period) + { + return period.getYears() != 0 || period.getMonths() != 0; + } + } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 61cec84e1ecf..60263d13dcf7 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -21,7 +21,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; +import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.Period; @@ -98,11 +100,8 @@ public ComposingReindexingRuleProvider( { this.providers = Objects.requireNonNull(providers, "providers cannot be null"); - // Validate that no provider in the list is null - for (int i = 0; i < providers.size(); i++) { - if (providers.get(i) == null) { - throw new NullPointerException("provider at index " + i + " is null"); - } + for (ReindexingRuleProvider provider : providers) { + Objects.requireNonNull(provider, "providers list contains null element"); } } @@ -126,13 +125,16 @@ public boolean isReady() } @Override - public List getCondensedAndSortedPeriods(DateTime referenceTime) + public @NotNull List getCondensedAndSortedPeriods(DateTime referenceTime) { // Collect all unique periods from all providers, sorted ascending return providers.stream() .flatMap(p -> p.getCondensedAndSortedPeriods(referenceTime).stream()) .distinct() - .sorted(Comparator.comparing(Period::toStandardDuration)) + .sorted(Comparator.comparingLong(period -> { + DateTime endTime = referenceTime.plus(period); + return new Duration(referenceTime, endTime).getMillis(); + })) .collect(Collectors.toList()); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index fe3a37a50a96..437c761adf69 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.common.config.Configs; +import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Interval; @@ -179,7 +180,7 @@ public List getTuningConfigRules() } @Override - public List getCondensedAndSortedPeriods(DateTime referenceTime) + public @NotNull List getCondensedAndSortedPeriods(DateTime referenceTime) { return Stream.of( reindexingFilterRules, @@ -253,14 +254,13 @@ public List getTuningConfigRules(Interval interval, */ private List getApplicableRules(List rules, Interval interval, DateTime referenceTime) { - boolean areRulesAdditive = false; List applicableRules = new ArrayList<>(); for (T rule : rules) { - areRulesAdditive = rule.isAdditive(); if (rule.appliesTo(interval, referenceTime) == ReindexingRule.AppliesToMode.FULL) { applicableRules.add(rule); } } + boolean areRulesAdditive = !rules.isEmpty() && rules.get(0).isAdditive(); if (!areRulesAdditive && applicableRules.size() > 1) { // if rules are not additive, I want the period where (referenceTime - period) is the oldest date of all the rules T selectedRule = Collections.min( diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java index 0407bf010ddf..b0854c79a52e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java @@ -38,7 +38,7 @@ public interface ReindexingRule *

    *
  • PARTIAL: The rule applies to part of the interval.
  • *
  • FULL: The rule applies to the entire interval.
  • - *
  • NONE: The rule does not apply to the interval at all.
  • NONE: The rule does not apply to the interval at all. *
*/ enum AppliesToMode diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 023277ff3429..574269909bc8 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -25,6 +25,7 @@ import org.joda.time.Interval; import org.joda.time.Period; +import javax.annotation.Nonnull; import java.util.List; /** @@ -75,6 +76,7 @@ default boolean isReady() * Ascending order means from shortest to longest period. For example, [P1D, P7D, P30D]. *

*/ + @Nonnull List getCondensedAndSortedPeriods(DateTime referenceTime); /** diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 499c06f82b62..97d89ceaa6ad 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -68,7 +68,7 @@ public void test_constructor_nullProviderInList_throwsNullPointerException() () -> new ComposingReindexingRuleProvider(providers) ); - Assert.assertTrue(exception.getMessage().contains("index 1")); + Assert.assertTrue(exception.getMessage().contains("providers list contains null element")); } @Test @@ -160,7 +160,7 @@ public void test_getGranularityRules_compositingBehavior() public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() { ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(7)); - ReindexingFilterRule rule2 = createFilterRule("rule2", Period.days(30)); + ReindexingFilterRule rule2 = createFilterRule("rule2", Period.months(1)); ReindexingFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder() @@ -174,10 +174,9 @@ public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() List result = composing.getCondensedAndSortedPeriods(REFERENCE_TIME); - // Should be deduplicated and sorted: [P7D, P30D] Assert.assertEquals(2, result.size()); Assert.assertEquals(Period.days(7), result.get(0)); - Assert.assertEquals(Period.days(30), result.get(1)); + Assert.assertEquals(Period.months(1), result.get(1)); } From 420f3b2576d0a3b3da1020c8208ee5ecf19c7229 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 17:41:42 -0600 Subject: [PATCH 18/90] Trying to transform cascadingreindexingtemplate to a compaction state is going to be a bad time --- .../druid/indexing/compact/CascadingReindexingTemplate.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index c01a6f3d5272..6d1c879cb978 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -49,6 +49,7 @@ import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; +import org.apache.druid.timeline.CompactionState; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -387,6 +388,11 @@ private List createJobsForSearchInterval( ); } + @Override + public CompactionState toCompactionState() { + throw new UnsupportedOperationException("CascadingReindexingTemplate cannot be transformed to a CompactionState object"); + } + // Legacy fields from DataSourceCompactionConfig that are not used by this template @Nullable From bf2e02d3527661aa53fe971e59c98e381ed2f342 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 18:00:08 -0600 Subject: [PATCH 19/90] refactor the location of the reindexing filter rule optimizer --- .../compact/CascadingReindexingTemplate.java | 5 +- .../ReindexingFilterRuleOptimizer.java | 161 +++++++ .../ReindexingFilterRuleOptimizerTest.java | 399 ++++++++++++++++++ .../compaction/ReindexingFilterRule.java | 124 ------ .../compaction/ReindexingFilterRuleTest.java | 368 ---------------- 5 files changed, 563 insertions(+), 494 deletions(-) create mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java create mode 100644 indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 6d1c879cb978..f5902de379ff 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -180,7 +180,7 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() if (shouldOptimizeFilterRules(candidate, config)) { // Compute the minimal set of filter rules needed for this candidate - NotDimFilter reducedTransformSpecFilter = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter reducedTransformSpecFilter = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, (NotDimFilter) config.getTransformSpec().getFilter(), params.getFingerprintMapper() @@ -389,7 +389,8 @@ private List createJobsForSearchInterval( } @Override - public CompactionState toCompactionState() { + public CompactionState toCompactionState() + { throw new UnsupportedOperationException("CascadingReindexingTemplate cannot be transformed to a CompactionState object"); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java new file mode 100644 index 000000000000..fa0f0f5cddeb --- /dev/null +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java @@ -0,0 +1,161 @@ +/* + * 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.indexing.compact; + +import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.timeline.CompactionState; +import org.apache.druid.timeline.DataSegment; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Optimization utilities for filter rule application during compaction. + * + *

When filter rules are applied incrementally (e.g., cascading reindexing, + * conditional compaction), segments may already have some filters applied from + * previous compaction runs. This class analyzes segment fingerprints to compute + * the minimal set of filters still needed, avoiding redundant bitmap operations. + */ +public class ReindexingFilterRuleOptimizer +{ + private static final Logger LOG = new Logger(ReindexingFilterRuleOptimizer.class); + + /** + * Computes the required set of filter rules to be applied for the given compaction candidate. + *

+ * We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces + * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. + *

+ * + * @param candidateSegments the compaction candidate + * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) + * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints + * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied + */ + @Nullable + public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( + CompactionCandidate candidateSegments, + NotDimFilter expectedFilter, + IndexingStateFingerprintMapper fingerprintMapper + ) + { + List expectedFilters; + if (!(expectedFilter.getField() instanceof OrDimFilter)) { + expectedFilters = Collections.singletonList(expectedFilter.getField()); + } else { + expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); + } + + // Collect unique fingerprints + Set uniqueFingerprints = candidateSegments.getSegments().stream() + .map(DataSegment::getIndexingStateFingerprint) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + if (uniqueFingerprints.isEmpty()) { + // no fingerprints means that no candidate segments have transforms to compare against. Return all filters eagerly. + return expectedFilter; + } + + // Accumulate filters that haven't been applied across all fingerprints + Set unappliedRules = new HashSet<>(); + + for (String fingerprint : uniqueFingerprints) { + CompactionState state = fingerprintMapper.getStateForFingerprint(fingerprint).orElse(null); + + if (state == null) { + // Safety: if state is missing, return all filters eagerly since we can't determine applied filters + return expectedFilter; + } + + // Extract applied filters from the CompactionState into a Set + Set appliedFilters = extractAppliedFilters(state); + + // If transform spec or filter for the CompactionState is null, return all expected filters eagerly + if (appliedFilters == null) { + return expectedFilter; + } + + // Check which expected filters are NOT in the applied set and add them to unappliedRules + for (DimFilter expected : expectedFilters) { + if (!appliedFilters.contains(expected)) { + unappliedRules.add(expected); + } + } + } + + LOG.debug( + "Computed [%d] unapplied rules out of [%d] possible rules for candidate", + unappliedRules.size(), + expectedFilters.size() + ); + + // If all filters were applied, return null + if (unappliedRules.isEmpty()) { + return null; + } + + // Return the delta as NOT(OR(unapplied filters)) + return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); + } + + /** + * Extracts the set of applied filters from a {@link CompactionState}. + * + * @param state the {@link CompactionState} to extract applied filters from + * @return the set of applied filters, or null if transform spec or filter is null (indicating 0 applied filters) + */ + @Nullable + private static Set extractAppliedFilters(CompactionState state) + { + if (state.getTransformSpec() == null) { + return null; + } + + DimFilter filter = state.getTransformSpec().getFilter(); + if (filter == null) { + return null; + } + + if (!(filter instanceof NotDimFilter)) { + return Collections.emptySet(); + } + + DimFilter inner = ((NotDimFilter) filter).getField(); + + if (inner instanceof OrDimFilter) { + return new HashSet<>(((OrDimFilter) inner).getFields()); + } else { + return Collections.singleton(inner); + } + } +} diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java new file mode 100644 index 000000000000..fbf72274e662 --- /dev/null +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java @@ -0,0 +1,399 @@ +/* + * 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.indexing.compact; + +import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.TestDataSource; +import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; +import org.apache.druid.segment.metadata.HeapMemoryIndexingStateStorage; +import org.apache.druid.segment.metadata.IndexingStateCache; +import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.timeline.CompactionState; +import org.apache.druid.timeline.DataSegment; +import org.apache.druid.timeline.SegmentId; +import org.joda.time.DateTime; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class ReindexingFilterRuleOptimizerTest +{ + private static final DataSegment WIKI_SEGMENT + = DataSegment.builder(SegmentId.of(TestDataSource.WIKI, Intervals.of("2013-01-01/PT1H"), "v1", 0)) + .size(100_000_000L) + .build(); + + private HeapMemoryIndexingStateStorage indexingStateStorage; + private IndexingStateCache indexingStateCache; + private IndexingStateFingerprintMapper fingerprintMapper; + + @Before + public void setUp() + { + indexingStateStorage = new HeapMemoryIndexingStateStorage(); + indexingStateCache = new IndexingStateCache(); + fingerprintMapper = new DefaultIndexingStateFingerprintMapper( + indexingStateCache, + new DefaultObjectMapper() + ); + } + + @Test + public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + NotDimFilter expectedFilter = new NotDimFilter(filterA); + + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void test_computeRequiredSetOfFilterRulesForCandidate_oneFilter_nothingToApply() + { + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + CompactionState state = createStateWithFilters(filterB); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + NotDimFilter expectedFilter = new NotDimFilter(filterB); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertNull(result); + } + + @Test + public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithFilters(filterA, filterB, filterC); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertNull(result); + } + + @Test + public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithoutFilters(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(3, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state = createStateWithFilters(filterA, filterB); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + DimFilter filterD = new SelectorDimFilter("country", "DE", null); + + CompactionState state1 = createStateWithFilters(filterA, filterB); + CompactionState state2 = createStateWithFilters(filterA, filterC); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); + + Assert.assertEquals(expectedSet, resultSet); + } + + @Test + public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state1 = createStateWithFilters(filterA); + CompactionState state2 = createStateWithFilters(filterA); + DateTime now = DateTimes.nowUtc(); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Set resultSet = new HashSet<>(innerOr.getFields()); + + Assert.assertEquals(2, resultSet.size()); + Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + // No state persisted for fp1 + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + @Test + public void testComputeRequiredFilters_TransformSpecWithSingleFilter() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionState state = createStateWithSingleFilter(filterA); + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); + syncCacheFromManager(); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + OrDimFilter innerOr = (OrDimFilter) result.getField(); + Assert.assertEquals(2, innerOr.getFields().size()); + Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); + } + + @Test + public void testComputeRequiredFilters_SegmentsWithNoFingerprints() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + DimFilter filterB = new SelectorDimFilter("country", "UK", null); + DimFilter filterC = new SelectorDimFilter("country", "FR", null); + + CompactionCandidate candidate = createCandidateWithNullFingerprints(3); + + NotDimFilter expectedFilter = new NotDimFilter( + new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) + ); + + NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + candidate, + expectedFilter, + fingerprintMapper + ); + + Assert.assertEquals(expectedFilter, result); + } + + + + + // Helper methods for filter tests + + private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) + { + List segments = Arrays.stream(fingerprints) + .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) + .collect(Collectors.toList()); + return CompactionCandidate.from(segments, null); + } + + private CompactionCandidate createCandidateWithNullFingerprints(int count) + { + List segments = new ArrayList<>(); + for (int i = 0; i < count; i++) { + segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); + } + return CompactionCandidate.from(segments, null); + } + + private CompactionState createStateWithFilters(DimFilter... filters) + { + OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); + NotDimFilter notFilter = new NotDimFilter(orFilter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithSingleFilter(DimFilter filter) + { + NotDimFilter notFilter = new NotDimFilter(filter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + + return new CompactionState( + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null + ); + } + + private CompactionState createStateWithoutFilters() + { + return new CompactionState( + null, + null, + null, + null, + IndexSpec.getDefault(), + null, + null + ); + } + + /** + * Helper to sync the cache with states stored in the manager (for tests that persist states). + */ + private void syncCacheFromManager() + { + indexingStateCache.resetIndexingStatesForPublishedSegments(indexingStateStorage.getAllStoredStates()); + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index fc20c0a4612e..d79d05f6e30e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -21,24 +21,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.OrDimFilter; -import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; -import org.apache.druid.timeline.CompactionState; -import org.apache.druid.timeline.DataSegment; import org.joda.time.Period; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; /** * A compaction filter rule that specifies rows to remove from segments older than a specified period. @@ -70,8 +58,6 @@ */ public class ReindexingFilterRule extends AbstractReindexingRule { - private static final Logger LOG = new Logger(ReindexingFilterRule.class); - private final DimFilter filter; @JsonCreator @@ -97,114 +83,4 @@ public DimFilter getFilter() { return filter; } - - /** - * Computes the required set of filter rules to be applied for the given compaction candidate. - *

- * We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces - * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. - *

- * - * @param candidateSegments the compaction candidate - * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) - * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints - * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied - */ - @Nullable - public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( - CompactionCandidate candidateSegments, - NotDimFilter expectedFilter, - IndexingStateFingerprintMapper fingerprintMapper - ) - { - List expectedFilters; - if (!(expectedFilter.getField() instanceof OrDimFilter)) { - expectedFilters = Collections.singletonList(expectedFilter.getField()); - } else { - expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); - } - - // Collect unique fingerprints - Set uniqueFingerprints = candidateSegments.getSegments().stream() - .map(DataSegment::getIndexingStateFingerprint) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - if (uniqueFingerprints.isEmpty()) { - // no fingerprints means that no candidate segments have transforms to compare against. Return all filters eagerly. - return expectedFilter; - } - - // Accumulate filters that haven't been applied across all fingerprints - Set unappliedRules = new HashSet<>(); - - for (String fingerprint : uniqueFingerprints) { - CompactionState state = fingerprintMapper.getStateForFingerprint(fingerprint).orElse(null); - - if (state == null) { - // Safety: if state is missing, return all filters eagerly since we can't determine applied filters - return expectedFilter; - } - - // Extract applied filters from the CompactionState into a Set - Set appliedFilters = extractAppliedFilters(state); - - // If transform spec or filter for the CompactionState is null, return all expected filters eagerly - if (appliedFilters == null) { - return expectedFilter; - } - - // Check which expected filters are NOT in the applied set and add them to unappliedRules - for (DimFilter expected : expectedFilters) { - if (!appliedFilters.contains(expected)) { - unappliedRules.add(expected); - } - } - } - - LOG.debug( - "Computed [%d] unapplied rules out of [%d] possible rules for candidate", - unappliedRules.size(), - expectedFilters.size() - ); - - // If all filters were applied, return null - if (unappliedRules.isEmpty()) { - return null; - } - - // Return the delta as NOT(OR(unapplied filters)) - return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); - } - - /** - * Extracts the set of applied filters from a {@link CompactionState}. - * - * @param state the {@link CompactionState} to extract applied filters from - * @return the set of applied filters, or null if transform spec or filter is null (indicating 0 applied filters) - */ - @Nullable - private static Set extractAppliedFilters(CompactionState state) - { - if (state.getTransformSpec() == null) { - return null; - } - - DimFilter filter = state.getTransformSpec().getFilter(); - if (filter == null) { - return null; - } - - if (!(filter instanceof NotDimFilter)) { - return Collections.emptySet(); - } - - DimFilter inner = ((NotDimFilter) filter).getField(); - - if (inner instanceof OrDimFilter) { - return new HashSet<>(((OrDimFilter) inner).getFields()); - } else { - return Collections.singleton(inner); - } - } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index 15d283cff92f..85c43ea8f175 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -19,47 +19,21 @@ package org.apache.druid.server.compaction; -import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.query.filter.SelectorDimFilter; -import org.apache.druid.segment.IndexSpec; -import org.apache.druid.segment.TestDataSource; -import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; -import org.apache.druid.segment.metadata.HeapMemoryIndexingStateStorage; -import org.apache.druid.segment.metadata.IndexingStateCache; -import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; -import org.apache.druid.segment.transform.CompactionTransformSpec; -import org.apache.druid.timeline.CompactionState; -import org.apache.druid.timeline.DataSegment; -import org.apache.druid.timeline.SegmentId; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - public class ReindexingFilterRuleTest { private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); - private static final DataSegment WIKI_SEGMENT - = DataSegment.builder(SegmentId.of(TestDataSource.WIKI, Intervals.of("2013-01-01/PT1H"), "v1", 0)) - .size(100_000_000L) - .build(); - private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); private final ReindexingFilterRule rule = new ReindexingFilterRule( "test-filter-rule", @@ -68,21 +42,6 @@ public class ReindexingFilterRuleTest testFilter ); - private HeapMemoryIndexingStateStorage indexingStateStorage; - private IndexingStateCache indexingStateCache; - private IndexingStateFingerprintMapper fingerprintMapper; - - @Before - public void setUp() - { - indexingStateStorage = new HeapMemoryIndexingStateStorage(); - indexingStateCache = new IndexingStateCache(); - fingerprintMapper = new DefaultIndexingStateFingerprintMapper( - indexingStateCache, - new DefaultObjectMapper() - ); - } - @Test public void test_isAdditive_returnsTrue() { @@ -371,331 +330,4 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() yearRule.appliesTo(afterThreshold, REFERENCE_TIME) ); } - - // ============================ - // computeRequiredSetOfFilterRulesForCandidate tests - // ============================ - - @Test - public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - NotDimFilter expectedFilter = new NotDimFilter(filterA); - - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - @Test - public void test_computeRequiredSetOfFilterRulesForCandidate_oneFilter_nothingToApply() - { - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - CompactionState state = createStateWithFilters(filterB); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - NotDimFilter expectedFilter = new NotDimFilter(filterB); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertNull(result); - } - - @Test - public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithFilters(filterA, filterB, filterC); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertNull(result); - } - - @Test - public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithoutFilters(); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Assert.assertEquals(3, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - DimFilter filterD = new SelectorDimFilter("country", "DE", null); - - CompactionState state = createStateWithFilters(filterA, filterB); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); - - Assert.assertEquals(expectedSet, resultSet); - } - - @Test - public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - DimFilter filterD = new SelectorDimFilter("country", "DE", null); - - CompactionState state1 = createStateWithFilters(filterA, filterB); - CompactionState state2 = createStateWithFilters(filterA, filterC); - DateTime now = DateTimes.nowUtc(); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); - - Assert.assertEquals(expectedSet, resultSet); - } - - @Test - public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state1 = createStateWithFilters(filterA); - CompactionState state2 = createStateWithFilters(filterA); - DateTime now = DateTimes.nowUtc(); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state1, now); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp2", state2, now); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Set resultSet = new HashSet<>(innerOr.getFields()); - - Assert.assertEquals(2, resultSet.size()); - Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - // No state persisted for fp1 - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - @Test - public void testComputeRequiredFilters_TransformSpecWithSingleFilter() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionState state = createStateWithSingleFilter(filterA); - indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); - syncCacheFromManager(); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - OrDimFilter innerOr = (OrDimFilter) result.getField(); - Assert.assertEquals(2, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); - } - - @Test - public void testComputeRequiredFilters_SegmentsWithNoFingerprints() - { - DimFilter filterA = new SelectorDimFilter("country", "US", null); - DimFilter filterB = new SelectorDimFilter("country", "UK", null); - DimFilter filterC = new SelectorDimFilter("country", "FR", null); - - CompactionCandidate candidate = createCandidateWithNullFingerprints(3); - - NotDimFilter expectedFilter = new NotDimFilter( - new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) - ); - - NotDimFilter result = ReindexingFilterRule.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); - - Assert.assertEquals(expectedFilter, result); - } - - // Helper methods for filter tests - - private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) - { - List segments = Arrays.stream(fingerprints) - .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) - .collect(Collectors.toList()); - return CompactionCandidate.from(segments, null); - } - - private CompactionCandidate createCandidateWithNullFingerprints(int count) - { - List segments = new ArrayList<>(); - for (int i = 0; i < count; i++) { - segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); - } - return CompactionCandidate.from(segments, null); - } - - private CompactionState createStateWithFilters(DimFilter... filters) - { - OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); - NotDimFilter notFilter = new NotDimFilter(orFilter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); - - return new CompactionState( - null, - null, - null, - transformSpec, - IndexSpec.getDefault(), - null, - null - ); - } - - private CompactionState createStateWithSingleFilter(DimFilter filter) - { - NotDimFilter notFilter = new NotDimFilter(filter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); - - return new CompactionState( - null, - null, - null, - transformSpec, - IndexSpec.getDefault(), - null, - null - ); - } - - private CompactionState createStateWithoutFilters() - { - return new CompactionState( - null, - null, - null, - null, - IndexSpec.getDefault(), - null, - null - ); - } - - /** - * Helper to sync the cache with states stored in the manager (for tests that persist states). - */ - private void syncCacheFromManager() - { - indexingStateCache.resetIndexingStatesForPublishedSegments(indexingStateStorage.getAllStoredStates()); - } } From 5b4f3d2289098512da63e71081acb84e92d24aa0 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 19:03:24 -0600 Subject: [PATCH 20/90] Refactor this idea of additivity and how it works for building configs --- .../compact/CascadingReindexingTemplate.java | 117 ++----------- .../ComposingReindexingRuleProvider.java | 48 ++--- .../InlineReindexingRuleProvider.java | 68 +++++--- .../compaction/ReindexingConfigBuilder.java | 165 ++++++++++++++++++ .../compaction/ReindexingDimensionsRule.java | 6 - .../compaction/ReindexingFilterRule.java | 6 - .../compaction/ReindexingGranularityRule.java | 6 - .../compaction/ReindexingIOConfigRule.java | 6 - .../compaction/ReindexingMetricsRule.java | 6 - .../compaction/ReindexingProjectionRule.java | 6 - .../server/compaction/ReindexingRule.java | 13 -- .../compaction/ReindexingRuleProvider.java | 57 +++--- .../ReindexingTuningConfigRule.java | 6 - .../ComposingReindexingRuleProviderTest.java | 75 ++++++-- .../InlineReindexingRuleProviderTest.java | 28 ++- .../ReindexingDimensionsRuleTest.java | 7 - .../compaction/ReindexingFilterRuleTest.java | 7 - .../ReindexingGranularityRuleTest.java | 7 - .../ReindexingIOConfigRuleTest.java | 7 - .../compaction/ReindexingMetricsRuleTest.java | 7 - .../ReindexingProjectionRuleTest.java | 7 - .../ReindexingTuningConfigRuleTest.java | 7 - 22 files changed, 353 insertions(+), 309 deletions(-) create mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index f5902de379ff..d748e1b26991 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -30,19 +30,12 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; -import org.apache.druid.server.compaction.ReindexingDimensionsRule; -import org.apache.druid.server.compaction.ReindexingFilterRule; -import org.apache.druid.server.compaction.ReindexingGranularityRule; -import org.apache.druid.server.compaction.ReindexingIOConfigRule; -import org.apache.druid.server.compaction.ReindexingMetricsRule; -import org.apache.druid.server.compaction.ReindexingProjectionRule; +import org.apache.druid.server.compaction.ReindexingConfigBuilder; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; -import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -60,7 +53,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; /** * Template to perform period-based cascading reindexing. {@link ReindexingRule} are provided by a {@link ReindexingRuleProvider} @@ -247,16 +239,10 @@ public List createCompactionJobs( List intervals = generateSearchIntervals(sortedPeriods, currentTime); for (Interval reindexingInterval : intervals) { - InlineSchemaDataSourceCompactionConfig.Builder builder = InlineSchemaDataSourceCompactionConfig.builder() - .forDataSource(dataSource) - .withTaskPriority(taskPriority) - .withInputSegmentSizeBytes(inputSegmentSizeBytes) - .withEngine(engine) - .withTaskContext(taskContext) - .withSkipOffsetFromLatest(Period.ZERO); + InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); - // Apply all applicable reindexing rules to the builder - int ruleCount = applyRulesToBuilder(builder, reindexingInterval, currentTime); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder(ruleProvider, reindexingInterval, currentTime); + int ruleCount = configBuilder.applyTo(builder); if (ruleCount > 0) { LOG.info("Creating reindexing jobs for interval[%s] with [%d] rules selected", reindexingInterval, ruleCount); @@ -275,6 +261,17 @@ public List createCompactionJobs( return allJobs; } + private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() + { + return InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource(dataSource) + .withTaskPriority(taskPriority) + .withInputSegmentSizeBytes(inputSegmentSizeBytes) + .withEngine(engine) + .withTaskContext(taskContext) + .withSkipOffsetFromLatest(Period.ZERO); + } + private List generateSearchIntervals(List sortedPeriods, DateTime referenceTime) { List intervals = new ArrayList<>(sortedPeriods.size()); @@ -291,90 +288,6 @@ private List generateSearchIntervals(List sortedPeriods, DateT return intervals; } - /** - * Applies all applicable reindexing rules to the builder for the given interval. - * - * @return number of rules applied - */ - private int applyRulesToBuilder( - InlineSchemaDataSourceCompactionConfig.Builder builder, - Interval reindexingInterval, - DateTime referenceTime - ) - { - int ruleCount = 0; - - // Granularity rules (non-additive, take first) - List granularityRules = ruleProvider.getGranularityRules(reindexingInterval, referenceTime); - if (!granularityRules.isEmpty()) { - LOG.info("Applying granularity rule %s for interval %s", granularityRules.get(0).getId(), reindexingInterval); - builder.withGranularitySpec(granularityRules.get(0).getGranularityConfig()); - ruleCount += 1; - } - - // Tuning config rules (non-additive, take first) - List tuningConfigRules = ruleProvider.getTuningConfigRules(reindexingInterval, referenceTime); - if (!tuningConfigRules.isEmpty()) { - LOG.info("Applying tuning config rule %s for interval %s", tuningConfigRules.get(0).getId(), reindexingInterval); - builder.withTuningConfig(tuningConfigRules.get(0).getTuningConfig()); - ruleCount += 1; - } - - // Metrics rules (non-additive, take first) - List metricsRules = ruleProvider.getMetricsRules(reindexingInterval, referenceTime); - if (!metricsRules.isEmpty()) { - LOG.info("Applying metrics rule %s for interval %s", metricsRules.get(0).getId(), reindexingInterval); - builder.withMetricsSpec(metricsRules.get(0).getMetricsSpec()); - ruleCount += 1; - } - - // Dimensions rules (non-additive, take first) - List dimensionsRules = ruleProvider.getDimensionsRules(reindexingInterval, referenceTime); - if (!dimensionsRules.isEmpty()) { - LOG.info("Applying dimensions rule %s for interval %s", dimensionsRules.get(0).getId(), reindexingInterval); - builder.withDimensionsSpec(dimensionsRules.get(0).getDimensionsSpec()); - ruleCount += 1; - } - - // IO config rules (non-additive, take first) - List ioConfigRules = ruleProvider.getIOConfigRules(reindexingInterval, referenceTime); - if (!ioConfigRules.isEmpty()) { - LOG.info("Applying IO config rule %s for interval %s", ioConfigRules.get(0).getId(), reindexingInterval); - builder.withIoConfig(ioConfigRules.get(0).getIoConfig()); - ruleCount += 1; - } - - // Projection rules (additive, combine all) - List projectionRules = ruleProvider.getProjectionRules(reindexingInterval, referenceTime); - if (!projectionRules.isEmpty()) { - LOG.info("Applying [%d] projection rules for interval %s", projectionRules.size(), reindexingInterval); - builder.withProjections( - projectionRules.stream() - .flatMap(rule -> rule.getProjections().stream()) - .collect(Collectors.toList()) - ); - ruleCount += projectionRules.size(); - } - - // Filter rules (additive, combine with OR and wrap in NOT) - List filterRules = ruleProvider.getFilterRules(reindexingInterval, referenceTime); - if (!filterRules.isEmpty()) { - LOG.info("Applying up to [%d] filter rules for interval %s", filterRules.size(), reindexingInterval); - List removeConditions = filterRules.stream() - .map(ReindexingFilterRule::getFilter) - .collect(Collectors.toList()); - - DimFilter removeFilter = removeConditions.size() == 1 - ? removeConditions.get(0) - : new OrDimFilter(removeConditions); - DimFilter finalFilter = new NotDimFilter(removeFilter); - builder.withTransformSpec(new CompactionTransformSpec(finalFilter)); - ruleCount += filterRules.size(); - } - - return ruleCount; - } - private List createJobsForSearchInterval( CompactionJobTemplate template, Interval searchInterval, diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 60263d13dcf7..c78168593e5a 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -21,12 +21,13 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.Period; +import javax.annotation.Nullable; +import javax.validation.constraints.NotNull; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -169,13 +170,14 @@ public List getMetricsRules() } @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getMetricsRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getMetricsRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override @@ -189,13 +191,14 @@ public List getDimensionsRules() } @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getDimensionsRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getDimensionsRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override @@ -209,13 +212,14 @@ public List getIOConfigRules() } @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getIOConfigRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getIOConfigRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override @@ -249,13 +253,14 @@ public List getGranularityRules() } @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getGranularityRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getGranularityRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override @@ -269,13 +274,14 @@ public List getTuningConfigRules() } @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingTuningConfigRule getTuningConfigRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getTuningConfigRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getTuningConfigRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index 437c761adf69..bd3eecb990e1 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -209,21 +209,24 @@ public List getFilterRules(Interval interval, DateTime ref } @Override - public List getMetricsRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingMetricsRules, interval, referenceTime); + return getApplicableRule(reindexingMetricsRules, interval, referenceTime); } @Override - public List getDimensionsRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingDimensionsRules, interval, referenceTime); + return getApplicableRule(reindexingDimensionsRules, interval, referenceTime); } @Override - public List getIOConfigRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingIOConfigRules, interval, referenceTime); + return getApplicableRule(reindexingIOConfigRules, interval, referenceTime); } @Override @@ -233,15 +236,17 @@ public List getProjectionRules(Interval interval, Date } @Override - public List getGranularityRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingGranularityRules, interval, referenceTime); + return getApplicableRule(reindexingGranularityRules, interval, referenceTime); } @Override - public List getTuningConfigRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingTuningConfigRule getTuningConfigRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingTuningConfigRules, interval, referenceTime); + return getApplicableRule(reindexingTuningConfigRules, interval, referenceTime); } /** @@ -249,8 +254,6 @@ public List getTuningConfigRules(Interval interval, *

* This provider implementation only returns rules that fully apply to the given interval. *

- * Any non-additive rule types will only return a single rule, even if multiple rules fully apply to the interval. The - * interval returned is the one with the oldest threshold (i.e., the largest period into the past from "now"). */ private List getApplicableRules(List rules, Interval interval, DateTime referenceTime) { @@ -260,21 +263,38 @@ private List getApplicableRules(List rules, Int applicableRules.add(rule); } } - boolean areRulesAdditive = !rules.isEmpty() && rules.get(0).isAdditive(); - if (!areRulesAdditive && applicableRules.size() > 1) { - // if rules are not additive, I want the period where (referenceTime - period) is the oldest date of all the rules - T selectedRule = Collections.min( - applicableRules, - Comparator.comparingLong(r -> { - DateTime threshold = referenceTime.minus(r.getPeriod()); - return threshold.getMillis(); - }) - ); - applicableRules = List.of(selectedRule); - } return applicableRules; } + /** + * Returns the single most applicable rule for the given interval. + *

+ * "most applicable" means if multiple rules match, the one returned is the one with the oldest + * threshold (i.e., the largest period into the past from "now"). + */ + @Nullable + private T getApplicableRule(List rules, Interval interval, DateTime referenceTime) + { + List applicableRules = new ArrayList<>(); + for (T rule : rules) { + if (rule.appliesTo(interval, referenceTime) == ReindexingRule.AppliesToMode.FULL) { + applicableRules.add(rule); + } + } + + if (applicableRules.isEmpty()) { + return null; + } + + return Collections.min( + applicableRules, + Comparator.comparingLong(r -> { + DateTime threshold = referenceTime.minus(r.getPeriod()); + return threshold.getMillis(); + }) + ); + } + @Override public boolean equals(Object o) { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java new file mode 100644 index 000000000000..5a4f3a0a1029 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -0,0 +1,165 @@ +/* + * 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.compaction; + +import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.apache.druid.java.util.common.logger.Logger; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Builds compaction configs by applying reindexing rules. + * Encapsulates the logic for combining additive rules and applying all rule types. + */ +public class ReindexingConfigBuilder +{ + private static final Logger LOG = new Logger(ReindexingConfigBuilder.class); + + private final ReindexingRuleProvider provider; + private final Interval interval; + private final DateTime referenceTime; + + public ReindexingConfigBuilder( + ReindexingRuleProvider provider, + Interval interval, + DateTime referenceTime + ) + { + this.provider = provider; + this.interval = interval; + this.referenceTime = referenceTime; + } + + /** + * Applies all applicable rules to the builder. + * + * @return number of rules applied + */ + public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) + { + int count = 0; + + count += applyIfPresent( + builder::withGranularitySpec, + provider.getGranularityRule(interval, referenceTime), + ReindexingGranularityRule::getGranularityConfig + ); + + count += applyIfPresent( + builder::withTuningConfig, + provider.getTuningConfigRule(interval, referenceTime), + ReindexingTuningConfigRule::getTuningConfig + ); + + count += applyIfPresent( + builder::withMetricsSpec, + provider.getMetricsRule(interval, referenceTime), + ReindexingMetricsRule::getMetricsSpec + ); + + count += applyIfPresent( + builder::withDimensionsSpec, + provider.getDimensionsRule(interval, referenceTime), + ReindexingDimensionsRule::getDimensionsSpec + ); + + count += applyIfPresent( + builder::withIoConfig, + provider.getIOConfigRule(interval, referenceTime), + ReindexingIOConfigRule::getIoConfig + ); + + count += applyProjectionRules(builder); + + count += applyFilterRules(builder); + + return count; + } + + // Generic helper for non-additive rules + private int applyIfPresent( + Consumer setter, + @Nullable R rule, + Function configExtractor + ) + { + if (rule != null) { + C config = configExtractor.apply(rule); + setter.accept(config); + LOG.debug( + "Applied rule %s for interval %s", + ((ReindexingRule) rule).getId(), interval + ); + return 1; + } + return 0; + } + + private int applyProjectionRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + { + List rules = provider.getProjectionRules(interval, referenceTime); + if (rules.isEmpty()) { + return 0; + } + + // Combine: flatMap all projections from all rules + List combined = rules.stream() + .flatMap(rule -> rule.getProjections().stream()) + .collect(Collectors.toList()); + + builder.withProjections(combined); + LOG.debug("Applied [%d] projection rules for interval %s", rules.size(), interval); + return rules.size(); + } + + private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + { + List rules = provider.getFilterRules(interval, referenceTime); + if (rules.isEmpty()) { + return 0; + } + + // Combine: OR all filters together, wrap in NOT + List removeConditions = rules.stream() + .map(ReindexingFilterRule::getFilter) + .collect(Collectors.toList()); + + DimFilter removeFilter = removeConditions.size() == 1 + ? removeConditions.get(0) + : new OrDimFilter(removeConditions); + + DimFilter finalFilter = new NotDimFilter(removeFilter); + builder.withTransformSpec(new CompactionTransformSpec(finalFilter)); + + LOG.debug("Applied [%d] filter rules for interval %s", rules.size(), interval); + return rules.size(); + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java index 1351f73111bf..cce4df21007c 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java @@ -72,12 +72,6 @@ public ReindexingDimensionsRule( this.dimensionsSpec = Objects.requireNonNull(dimensionsSpec, "dimensionsSpec cannot be null"); } - @Override - public boolean isAdditive() - { - return false; - } - @JsonProperty public UserCompactionTaskDimensionsConfig getDimensionsSpec() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index d79d05f6e30e..7b8fef6dcfbd 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -72,12 +72,6 @@ public ReindexingFilterRule( this.filter = Objects.requireNonNull(filter, "filter cannot be null"); } - @Override - public boolean isAdditive() - { - return true; - } - @JsonProperty public DimFilter getFilter() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java index 40ca19065ddb..afd590c34c02 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java @@ -70,12 +70,6 @@ public ReindexingGranularityRule( this.granularityConfig = Objects.requireNonNull(granularityConfig, "granularityConfig cannot be null"); } - @Override - public boolean isAdditive() - { - return false; - } - @JsonProperty public UserCompactionTaskGranularityConfig getGranularityConfig() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java index 5f1b1204a55b..d72429595c4e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java @@ -64,12 +64,6 @@ public ReindexingIOConfigRule( this.ioConfig = Objects.requireNonNull(ioConfig, "ioConfig cannot be null"); } - @Override - public boolean isAdditive() - { - return false; - } - @JsonProperty public UserCompactionTaskIOConfig getIoConfig() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java index 51d5d822c6b5..8d25ae5c8000 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java @@ -70,12 +70,6 @@ public ReindexingMetricsRule( this.metricsSpec = Objects.requireNonNull(metricsSpec, "metricsSpec cannot be null"); } - @Override - public boolean isAdditive() - { - return false; - } - @JsonProperty @Nonnull public AggregatorFactory[] getMetricsSpec() diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java index 9cb18899d7d7..5baa6910c341 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java @@ -75,12 +75,6 @@ public ReindexingProjectionRule( this.projections = Objects.requireNonNull(projections, "projections cannot be null"); } - @Override - public boolean isAdditive() - { - return true; - } - @JsonProperty public List getProjections() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java index b0854c79a52e..43c601beeea1 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java @@ -68,17 +68,4 @@ enum AppliesToMode */ AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTime); - /** - * Indicates whether the rule is additive, meaning it can be combined with other rules. - *

- * An additive rule can be merged with other rules of its type within the same interval. An example would be dimension - * filter rules that can be combined using OR logic. Such as, rule 1: filter out segments where country = 'US' OR - * rule 2: device = 'mobile'. - *

- *

- * A non-addditive rule cannot be combined with other rules of its type within the same interval. An example would be - * segment granularity rules, where only one granularity can be applied to a given interval. - *

- */ - boolean isAdditive(); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 574269909bc8..a865b3a8b302 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -26,6 +26,7 @@ import org.joda.time.Period; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.List; /** @@ -97,36 +98,39 @@ default boolean isReady() List getFilterRules(); /** - * Returns all reindexing metrics rules that apply to the given interval. + * Returns the matched reindexing metrics rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingMetricsRule} rules that apply to the given interval. + * @return {@link ReindexingMetricsRule} rule that applies to the given interval. */ - List getMetricsRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing metrics rules. - *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. - *

*/ List getMetricsRules(); /** - * Returns all reindexing dimensions rules that apply to the given interval. + * Returns the matched reindexing dimensions rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingDimensionsRule} rules that apply to the given interval. + * @return {@link ReindexingDimensionsRule} rule that applies to the given interval. */ - List getDimensionsRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing dimensions rules. @@ -134,16 +138,19 @@ default boolean isReady() List getDimensionsRules(); /** - * Returns all reindexing IO config rules that apply to the given interval. + * Returns the matched reindexing IO config rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingIOConfigRule} rules that apply to the given interval. + * @return {@link ReindexingIOConfigRule} that applies to the given interval. */ - List getIOConfigRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing IO config rules. @@ -168,16 +175,19 @@ default boolean isReady() List getProjectionRules(); /** - * Returns all reindexing granularity rules that apply to the given interval. + * Returns the matched reindexing granularity rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingGranularityRule} rules that apply to the given interval. + * @return {@link ReindexingGranularityRule} rule that applies to the given interval. */ - List getGranularityRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing granularity rules. @@ -185,15 +195,18 @@ default boolean isReady() List getGranularityRules(); /** - * Returns all reindexing tuning config rules that apply to the given interval. + * Returns the matched reindexing tuning config rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. */ - List getTuningConfigRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingTuningConfigRule getTuningConfigRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing tuning config rules. diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java index fe068272b137..52417532c3a7 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java @@ -73,12 +73,6 @@ public ReindexingTuningConfigRule( this.tuningConfig = Objects.requireNonNull(tuningConfig, "tuningConfig cannot be null"); } - @Override - public boolean isAdditive() - { - return false; - } - @JsonProperty public UserCompactionTaskQueryTuningConfig getTuningConfig() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 97d89ceaa6ad..d505c84e371a 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -135,7 +135,7 @@ public void test_getFilterRules_compositingBehavior() @Test public void test_getFilterRulesWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().filterRules(rules).build(), (provider, it) -> provider.getFilterRules(it.interval, it.time), createFilterRule("rule1", Period.days(7)), @@ -193,11 +193,11 @@ public void test_getMetricsRules_compositingBehavior() } @Test - public void test_getMetricsRulesWithInterval_compositingBehavior() + public void test_getMetricsRuleWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().metricsRules(rules).build(), - (provider, it) -> provider.getMetricsRules(it.interval, it.time), + (provider, it) -> provider.getMetricsRule(it.interval, it.time), createMetricsRule("rule1", Period.days(7)), createMetricsRule("rule2", Period.days(30)), ReindexingMetricsRule::getId @@ -217,11 +217,11 @@ public void test_getDimensionsRules_compositingBehavior() } @Test - public void test_getDimensionsRulesWithInterval_compositingBehavior() + public void test_getDimensionsRuleWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().dimensionsRules(rules).build(), - (provider, it) -> provider.getDimensionsRules(it.interval, it.time), + (provider, it) -> provider.getDimensionsRule(it.interval, it.time), createDimensionsRule("rule1", Period.days(7)), createDimensionsRule("rule2", Period.days(30)), ReindexingDimensionsRule::getId @@ -241,11 +241,11 @@ public void test_getIOConfigRules_compositingBehavior() } @Test - public void test_getIOConfigRulesWithInterval_compositingBehavior() + public void test_getIOConfigRuleWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().ioConfigRules(rules).build(), - (provider, it) -> provider.getIOConfigRules(it.interval, it.time), + (provider, it) -> provider.getIOConfigRule(it.interval, it.time), createIOConfigRule("rule1", Period.days(7)), createIOConfigRule("rule2", Period.days(30)), ReindexingIOConfigRule::getId @@ -267,7 +267,7 @@ public void test_getProjectionRules_compositingBehavior() @Test public void test_getProjectionRulesWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), (provider, it) -> provider.getProjectionRules(it.interval, it.time), createProjectionRule("rule1", Period.days(7)), @@ -289,11 +289,11 @@ public void test_getTuningConfigRules_compositingBehavior() } @Test - public void test_getTuningConfigRulesWithInterval_compositingBehavior() + public void test_getTuningConfigRuleWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().tuningConfigRules(rules).build(), - (provider, it) -> provider.getTuningConfigRules(it.interval, it.time), + (provider, it) -> provider.getTuningConfigRule(it.interval, it.time), createTuningConfigRule("rule1", Period.days(7)), createTuningConfigRule("rule2", Period.days(30)), ReindexingTuningConfigRule::getId @@ -301,11 +301,11 @@ public void test_getTuningConfigRulesWithInterval_compositingBehavior() } @Test - public void test_getGranularityRulesWithInterval_compositingBehavior() + public void test_getGranularityRuleWithInterval_compositingBehavior() { - testComposingBehaviorForRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().granularityRules(rules).build(), - (provider, it) -> provider.getGranularityRules(it.interval, it.time), + (provider, it) -> provider.getGranularityRule(it.interval, it.time), createGranularityRule("rule1", Period.days(7)), createGranularityRule("rule2", Period.days(30)), ReindexingGranularityRule::getId @@ -410,13 +410,52 @@ private void testComposingBehaviorForRuleType( Assert.assertTrue(result.isEmpty()); } + private void testComposingBehaviorForNonAdditiveRuleTypeWithInterval( + Function, ReindexingRuleProvider> providerFactory, + BiFunction ruleGetter, + T rule1, + T rule2, + Function idExtractor + ) + { + Interval interval = Intervals.of("2025-11-01/2025-11-15"); + + ReindexingRuleProvider provider1 = providerFactory.apply(ImmutableList.of(rule1)); + ReindexingRuleProvider provider2 = providerFactory.apply(ImmutableList.of(rule2)); + + ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( + ImmutableList.of(provider1, provider2) + ); + + T result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertNotNull(result); + Assert.assertEquals("rule1", idExtractor.apply(result)); + + ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, provider2) + ); + + result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertNotNull(result); + Assert.assertEquals("rule2", idExtractor.apply(result)); + + ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); + composing = new ComposingReindexingRuleProvider( + ImmutableList.of(emptyProvider, emptyProvider2) + ); + + result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); + Assert.assertNull(result); + } + /** * Tests composing behavior for getXxxRules(interval, time) - all three scenarios: * 1. First provider has rules → returns first provider's rules * 2. First provider empty → falls through to second provider * 3. Both providers empty → returns empty list */ - private void testComposingBehaviorForRuleTypeWithInterval( + private void testComposingBehaviorForAdditiveRuleTypeWithInterval( Function, ReindexingRuleProvider> providerFactory, BiFunction> ruleGetter, T rule1, diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index e4491e3341b1..c2cf1b287607 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -145,16 +145,13 @@ public void test_nonAdditiveRules_allScenarios() .granularityRules(ImmutableList.of(rule30d, rule60d, rule90d)) .build(); - List noMatch = provider.getGranularityRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); - Assert.assertTrue("No rules should match interval that's too recent", noMatch.isEmpty()); + Assert.assertNull(provider.getGranularityRule(INTERVAL_20_DAYS_OLD, REFERENCE_TIME)); - List oneMatch = provider.getGranularityRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("Only rule30d should match", 1, oneMatch.size()); - Assert.assertEquals("gran-30d", oneMatch.get(0).getId()); + ReindexingGranularityRule oneMatch = provider.getGranularityRule(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("gran-30d", oneMatch.getId()); - List multiMatch = provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("Only one non-additive rule should be returned", 1, multiMatch.size()); - Assert.assertEquals("Should return rule with oldest threshold (P90D)", "gran-90d", multiMatch.get(0).getId()); + ReindexingGranularityRule multiMatch = provider.getGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals("Should return rule with oldest threshold (P90D)", "gran-90d", multiMatch.getId()); } @Test @@ -181,23 +178,18 @@ public void test_allRuleTypesWireCorrectly_withInterval() Assert.assertEquals(1, provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); Assert.assertEquals("filter", provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); - Assert.assertEquals(1, provider.getMetricsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("metrics", provider.getMetricsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("metrics", provider.getMetricsRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals(1, provider.getDimensionsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("dimensions", provider.getDimensionsRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("dimensions", provider.getDimensionsRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals(1, provider.getIOConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("ioconfig", provider.getIOConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("ioconfig", provider.getIOConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); Assert.assertEquals(1, provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); Assert.assertEquals("projection", provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); - Assert.assertEquals(1, provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("granularity", provider.getGranularityRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("granularity", provider.getGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals(1, provider.getTuningConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("tuning", provider.getTuningConfigRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); } @Test diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java index 2983508a8498..cbb3de0f8b78 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java @@ -40,13 +40,6 @@ public class ReindexingDimensionsRuleTest new UserCompactionTaskDimensionsConfig(null) ); - @Test - public void test_isAdditive_returnsFalse() - { - // Dimensions rules are not additive - only one dimensions spec can apply - Assert.assertFalse(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index 85c43ea8f175..c2d88d75e70a 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -42,13 +42,6 @@ public class ReindexingFilterRuleTest testFilter ); - @Test - public void test_isAdditive_returnsTrue() - { - // Filter rules are additive - multiple filters can be combined - Assert.assertTrue(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java index d7ce293ec8af..98cacc2be561 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java @@ -41,13 +41,6 @@ public class ReindexingGranularityRuleTest new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) ); - @Test - public void test_isAdditive_returnsFalse() - { - // Granularity rules are not additive - only one granularity can apply - Assert.assertFalse(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index b879b22e7fc1..3846df03b473 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -40,13 +40,6 @@ public class ReindexingIOConfigRuleTest new UserCompactionTaskIOConfig(null) ); - @Test - public void test_isAdditive_returnsFalse() - { - // IO config rules are not additive - only one IO config can apply - Assert.assertFalse(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java index 4db355c845e6..591b66d80218 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java @@ -47,13 +47,6 @@ public class ReindexingMetricsRuleTest testMetrics ); - @Test - public void test_isAdditive_returnsFalse() - { - // Metrics rules are not additive - only one metrics spec can apply - Assert.assertFalse(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java index 3f1566bdcfcd..c35a5cca89ba 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java @@ -41,13 +41,6 @@ public class ReindexingProjectionRuleTest Collections.emptyList() ); - @Test - public void test_isAdditive_returnsTrue() - { - // Projection rules are additive - multiple projections can be combined - Assert.assertTrue(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index f7bde288a74e..b88122c1f8ce 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -42,13 +42,6 @@ public class ReindexingTuningConfigRuleTest ); - @Test - public void test_isAdditive_returnsFalse() - { - // Tuning config rules are not additive - only one tuning config can apply - Assert.assertFalse(rule.isAdditive()); - } - @Test public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() { From 6f5ead72f44149eabbe58bf2cb867d8cce25fe22 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 19:31:35 -0600 Subject: [PATCH 21/90] Add a missing test class --- .../ReindexingConfigBuilderTest.java | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java new file mode 100644 index 000000000000..d1e2b521b5c1 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -0,0 +1,210 @@ +/* + * 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.compaction; + +import com.google.common.collect.ImmutableList; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; +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.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class ReindexingConfigBuilderTest +{ + private static final Interval TEST_INTERVAL = Intervals.of("2024-11-01/2024-11-02"); + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-01-15"); + + @Test + public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount() + { + ReindexingRuleProvider provider = createFullyPopulatedProvider(); + InlineSchemaDataSourceCompactionConfig.Builder builder = + InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource("test_datasource"); + + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( + provider, + TEST_INTERVAL, + REFERENCE_TIME + ); + + int count = configBuilder.applyTo(builder); + + Assert.assertEquals(9, count); // 5 non-additive + 2 projection rules + 2 filter rules + + InlineSchemaDataSourceCompactionConfig config = builder.build(); + + Assert.assertNotNull(config.getGranularitySpec()); + Assert.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); + + Assert.assertNotNull(config.getTuningConfig()); + Assert.assertNotNull(config.getMetricsSpec()); + Assert.assertEquals(1, config.getMetricsSpec().length); + Assert.assertEquals("count", config.getMetricsSpec()[0].getName()); + + Assert.assertNotNull(config.getDimensionsSpec()); + Assert.assertNotNull(config.getIoConfig()); + + Assert.assertNotNull(config.getProjections()); + Assert.assertEquals(3, config.getProjections().size()); // 2 from rule1 + 1 from rule2 + + Assert.assertNotNull(config.getTransformSpec()); + DimFilter appliedFilter = config.getTransformSpec().getFilter(); + Assert.assertTrue(appliedFilter instanceof NotDimFilter); + + NotDimFilter notFilter = (NotDimFilter) appliedFilter; + Assert.assertTrue(notFilter.getField() instanceof OrDimFilter); + + OrDimFilter orFilter = (OrDimFilter) notFilter.getField(); + Assert.assertEquals(2, orFilter.getFields().size()); // 2 filters combined + } + + @Test + public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() + { + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); + InlineSchemaDataSourceCompactionConfig.Builder builder = + InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource("test_datasource"); + + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( + provider, + TEST_INTERVAL, + REFERENCE_TIME + ); + + int count = configBuilder.applyTo(builder); + + Assert.assertEquals(0, count); + + InlineSchemaDataSourceCompactionConfig config = builder.build(); + + Assert.assertNull(config.getGranularitySpec()); + Assert.assertNull(config.getTuningConfig()); + Assert.assertNull(config.getMetricsSpec()); + Assert.assertNull(config.getDimensionsSpec()); + Assert.assertNull(config.getIoConfig()); + Assert.assertNull(config.getProjections()); + Assert.assertNull(config.getTransformSpec()); + } + + private ReindexingRuleProvider createFullyPopulatedProvider() + { + ReindexingGranularityRule granularityRule = new ReindexingGranularityRule( + "gran-30d", + null, + Period.days(30), + new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + ); + + ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( + "tuning-30d", + null, + Period.days(30), + new UserCompactionTaskQueryTuningConfig(null, null, null, null, null, null, + null, null, null, null, null, null, + null, null, null, null, null, null, null) + ); + + ReindexingMetricsRule metricsRule = new ReindexingMetricsRule( + "metrics-30d", + null, + Period.days(30), + new AggregatorFactory[]{new CountAggregatorFactory("count")} + ); + + ReindexingDimensionsRule dimensionsRule = new ReindexingDimensionsRule( + "dims-30d", + null, + Period.days(30), + new UserCompactionTaskDimensionsConfig(null) + ); + + ReindexingIOConfigRule ioConfigRule = new ReindexingIOConfigRule( + "io-30d", + null, + Period.days(30), + new UserCompactionTaskIOConfig(null) + ); + + // Two projection rules (additive) + ReindexingProjectionRule projectionRule1 = new ReindexingProjectionRule( + "proj-30d", + null, + Period.days(30), + ImmutableList.of( + new AggregateProjectionSpec("proj1", null, null, null, + new AggregatorFactory[]{new CountAggregatorFactory("count1")}), + new AggregateProjectionSpec("proj2", null, null, null, + new AggregatorFactory[]{new CountAggregatorFactory("count2")}) + ) + ); + + ReindexingProjectionRule projectionRule2 = new ReindexingProjectionRule( + "proj-60d", + null, + Period.days(60), + ImmutableList.of( + new AggregateProjectionSpec("proj3", null, null, null, + new AggregatorFactory[]{new CountAggregatorFactory("count3")}) + ) + ); + + // Two filter rules (additive) + ReindexingFilterRule filterRule1 = new ReindexingFilterRule( + "filter-30d", + null, + Period.days(30), + new SelectorDimFilter("country", "US", null) + ); + + ReindexingFilterRule filterRule2 = new ReindexingFilterRule( + "filter-60d", + null, + Period.days(60), + new SelectorDimFilter("device", "mobile", null) + ); + + return InlineReindexingRuleProvider.builder() + .granularityRules(ImmutableList.of(granularityRule)) + .tuningConfigRules(ImmutableList.of(tuningConfigRule)) + .metricsRules(ImmutableList.of(metricsRule)) + .dimensionsRules(ImmutableList.of(dimensionsRule)) + .ioConfigRules(ImmutableList.of(ioConfigRule)) + .projectionRules(ImmutableList.of(projectionRule1, projectionRule2)) + .filterRules(ImmutableList.of(filterRule1, filterRule2)) + .build(); + } +} \ No newline at end of file From 6d1fc6e1d3dc27f69d3bae2ddaf247cc245b9744 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 19:35:15 -0600 Subject: [PATCH 22/90] fix checkstyle --- .../druid/server/compaction/ReindexingConfigBuilderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index d1e2b521b5c1..5e0cda0e8811 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -207,4 +207,4 @@ private ReindexingRuleProvider createFullyPopulatedProvider() .filterRules(ImmutableList.of(filterRule1, filterRule2)) .build(); } -} \ No newline at end of file +} From 6853b025f67b87b3e06691790ae68b73be9c412b Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 20:09:03 -0600 Subject: [PATCH 23/90] clean up a javadoc --- .../ReindexingFilterRuleOptimizer.java | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java index fa0f0f5cddeb..7209bed7697e 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java @@ -38,25 +38,26 @@ import java.util.stream.Collectors; /** - * Optimization utilities for filter rule application during compaction. + * Optimization utilities for filter rule application during reindexing * - *

When filter rules are applied incrementally (e.g., cascading reindexing, - * conditional compaction), segments may already have some filters applied from - * previous compaction runs. This class analyzes segment fingerprints to compute - * the minimal set of filters still needed, avoiding redundant bitmap operations. + *

+ * When reindexing with {@link org.apache.druid.server.compaction.ReindexingFilterRule}s, it is possible that candidate + * segments have already applied some or all of the filters in previous reindexing runs. Reapplying such filters would + * be wasteful and redundant. This class provides funcionality to optimize the set of filters to be applied by + * any given reindexing task. */ public class ReindexingFilterRuleOptimizer { private static final Logger LOG = new Logger(ReindexingFilterRuleOptimizer.class); /** - * Computes the required set of filter rules to be applied for the given compaction candidate. + * Computes the required set of filter rules to be applied for the given {@link CompactionCandidate}. *

* We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces - * the amount of work the compaction task needs to do and avoids re-applying filters that have already been applied. + * the amount of work the task needs to do while processing rows during reindexing. *

* - * @param candidateSegments the compaction candidate + * @param candidateSegments the {@link CompactionCandidate} * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied @@ -75,7 +76,6 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( expectedFilters = ((OrDimFilter) expectedFilter.getField()).getFields(); } - // Collect unique fingerprints Set uniqueFingerprints = candidateSegments.getSegments().stream() .map(DataSegment::getIndexingStateFingerprint) .filter(Objects::nonNull) @@ -86,7 +86,6 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( return expectedFilter; } - // Accumulate filters that haven't been applied across all fingerprints Set unappliedRules = new HashSet<>(); for (String fingerprint : uniqueFingerprints) { @@ -97,15 +96,12 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( return expectedFilter; } - // Extract applied filters from the CompactionState into a Set Set appliedFilters = extractAppliedFilters(state); - // If transform spec or filter for the CompactionState is null, return all expected filters eagerly if (appliedFilters == null) { return expectedFilter; } - // Check which expected filters are NOT in the applied set and add them to unappliedRules for (DimFilter expected : expectedFilters) { if (!appliedFilters.contains(expected)) { unappliedRules.add(expected); @@ -119,12 +115,10 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( expectedFilters.size() ); - // If all filters were applied, return null if (unappliedRules.isEmpty()) { return null; } - // Return the delta as NOT(OR(unapplied filters)) return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); } From 22004672780ac5d4858574eaf31f5dc25cf5abef Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 22 Jan 2026 20:16:35 -0600 Subject: [PATCH 24/90] trivial fixes --- .../druid/server/compaction/AbstractReindexingRule.java | 3 +++ .../server/compaction/ComposingReindexingRuleProvider.java | 5 +++-- .../server/compaction/InlineReindexingRuleProvider.java | 5 +++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index b9521dec9467..21424a1b3069 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -145,6 +145,9 @@ public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTi } } + /** + * Checks if a period contains months or years components which have variable lenghts and require special handling + */ private static boolean hasMonthsOrYears(Period period) { return period.getYears() != 0 || period.getMonths() != 0; diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index c78168593e5a..335053e7d2e4 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -26,8 +26,8 @@ import org.joda.time.Interval; import org.joda.time.Period; +import javax.annotation.Nonnull; import javax.annotation.Nullable; -import javax.validation.constraints.NotNull; import java.util.Collections; import java.util.Comparator; import java.util.List; @@ -126,7 +126,8 @@ public boolean isReady() } @Override - public @NotNull List getCondensedAndSortedPeriods(DateTime referenceTime) + @Nonnull + public List getCondensedAndSortedPeriods(DateTime referenceTime) { // Collect all unique periods from all providers, sorted ascending return providers.stream() diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index bd3eecb990e1..4a836e254661 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -22,12 +22,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.common.config.Configs; -import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.Period; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; @@ -180,7 +180,8 @@ public List getTuningConfigRules() } @Override - public @NotNull List getCondensedAndSortedPeriods(DateTime referenceTime) + @Nonnull + public List getCondensedAndSortedPeriods(DateTime referenceTime) { return Stream.of( reindexingFilterRules, From a0d68eb10bd10b2b9c15876a3439999a6979a238 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 23 Jan 2026 14:54:28 -0600 Subject: [PATCH 25/90] Prevent an edge case for a negative period --- .../compaction/AbstractReindexingRule.java | 29 ++++++---- .../AbstractReindexingRuleTest.java | 53 +++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index 21424a1b3069..72255c774ae5 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -89,18 +89,29 @@ private static void validatePeriodIsPositive(Period period) * Checks if a period with variable-length components (months/years) is positive. * * @param period the period to check - * @return true if any component is positive + * @return true if any component is positive and no components are negative */ private static boolean isPeriodPositive(Period period) { - return period.getYears() > 0 - || period.getMonths() > 0 - || period.getWeeks() > 0 - || period.getDays() > 0 - || period.getHours() > 0 - || period.getMinutes() > 0 - || period.getSeconds() > 0 - || period.getMillis() > 0; + boolean hasPositiveComponent = period.getYears() > 0 + || period.getMonths() > 0 + || period.getWeeks() > 0 + || period.getDays() > 0 + || period.getHours() > 0 + || period.getMinutes() > 0 + || period.getSeconds() > 0 + || period.getMillis() > 0; + + boolean hasNegativeComponent = period.getYears() < 0 + || period.getMonths() < 0 + || period.getWeeks() < 0 + || period.getDays() < 0 + || period.getHours() < 0 + || period.getMinutes() < 0 + || period.getSeconds() < 0 + || period.getMillis() < 0; + + return hasPositiveComponent && !hasNegativeComponent; } @JsonProperty diff --git a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java new file mode 100644 index 000000000000..3db32229e556 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java @@ -0,0 +1,53 @@ +/* + * 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.compaction; + +import org.apache.druid.query.filter.SelectorDimFilter; +import org.joda.time.Period; +import org.junit.Test; + +public class AbstractReindexingRuleTest +{ + @Test(expected = IllegalArgumentException.class) + public void test_constructor_positiveMonthsNegativeDays_throwsException() + { + Period period = Period.months(1).withDays(-40); + + new ReindexingFilterRule( + "test-rule", + null, + period, + new SelectorDimFilter("dim", "val", null) + ); + } + + @Test(expected = IllegalArgumentException.class) + public void test_constructor_positiveYearsNegativeMonths_throwsException() + { + Period period = new Period(1, -13, 0, 0, 0, 0, 0, 0); + + new ReindexingFilterRule( + "test-rule", + null, + period, + new SelectorDimFilter("dim", "val", null) + ); + } +} From bbb5bbdbe09113c61a35ef42f2021bb33e8efbdb Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 23 Jan 2026 22:48:42 -0600 Subject: [PATCH 26/90] fix a nasty bug opportunity --- .../compact/CascadingReindexingTemplate.java | 21 +++++++++++++++++++ .../DataSourceCompactibleSegmentIterator.java | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index d748e1b26991..e8fdbec3fc1f 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -43,6 +43,9 @@ import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.timeline.CompactionState; +import org.apache.druid.timeline.DataSegment; +import org.apache.druid.timeline.SegmentTimeline; +import org.apache.druid.timeline.TimelineObjectHolder; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -238,7 +241,25 @@ public List createCompactionJobs( } List intervals = generateSearchIntervals(sortedPeriods, currentTime); + SegmentTimeline timeline = jobParams.getTimeline(dataSource); + + if (timeline == null || timeline.isEmpty()) { + LOG.warn("Segment timeline null or empty for [%s] skipping creating compaction jobs.", dataSource); + return Collections.emptyList(); + } + + // Full data range covered by the timeline + TimelineObjectHolder first = timeline.first(); + TimelineObjectHolder last = timeline.last(); + Interval dataRange = new Interval(first.getInterval().getStart(), last.getInterval().getEnd()); + for (Interval reindexingInterval : intervals) { + + if (!reindexingInterval.overlaps(dataRange)) { + LOG.info("Search interval[%s] does not overlap with data range[%s], skipping", reindexingInterval, dataRange); + continue; + } + InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder(ruleProvider, reindexingInterval, currentTime); diff --git a/server/src/main/java/org/apache/druid/server/compaction/DataSourceCompactibleSegmentIterator.java b/server/src/main/java/org/apache/druid/server/compaction/DataSourceCompactibleSegmentIterator.java index 1994e87a6388..ed80ec967b6f 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/DataSourceCompactibleSegmentIterator.java +++ b/server/src/main/java/org/apache/druid/server/compaction/DataSourceCompactibleSegmentIterator.java @@ -509,7 +509,7 @@ skipInterval, new Interval(remainingStart, remainingEnd) } } - if (!remainingStart.equals(remainingEnd)) { + if (remainingStart.isBefore(remainingEnd)) { filteredIntervals.add(new Interval(remainingStart, remainingEnd)); } From 4e2dcb043392c259be0c56e2504c8ff3d4b38ab1 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 26 Jan 2026 17:23:57 -0600 Subject: [PATCH 27/90] Add support to filter on nested columns for MSQ reindexing using virtual columns with filter rules Fail native compaction with a descriptive output if they try to use virtual columns in their native reindexing supervisor Make a cascading reindexing optimizer method testable --- .../embedded/compact/AutoCompactionTest.java | 2 +- .../compact/CompactionSupervisorTest.java | 239 ++++++++++++------ .../common/task/NativeCompactionRunner.java | 8 + .../compact/CascadingReindexingTemplate.java | 12 +- .../ReindexingFilterRuleOptimizer.java | 35 +++ .../ClientCompactionTaskQuerySerdeTest.java | 2 +- .../task/CompactionTaskParallelRunTest.java | 2 +- .../common/task/CompactionTaskRunTest.java | 2 +- .../common/task/CompactionTaskTest.java | 4 +- .../task/NativeCompactionRunnerTest.java | 118 +++++++++ .../ReindexingFilterRuleOptimizerTest.java | 95 ++++++- .../msq/indexing/MSQCompactionRunner.java | 14 +- .../msq/indexing/MSQCompactionRunnerTest.java | 2 +- .../table/DataSegmentWithLocationTest.java | 19 +- .../transform/CompactionTransformSpec.java | 21 +- .../CompactionTransformSpecTest.java | 22 +- .../druid/timeline/DataSegmentTest.java | 60 ++++- .../compaction/ReindexingConfigBuilder.java | 29 ++- .../compaction/ReindexingFilterRule.java | 39 ++- .../AbstractReindexingRuleTest.java | 6 +- .../ComposingReindexingRuleProviderTest.java | 3 +- .../InlineReindexingRuleProviderTest.java | 2 +- .../NewestSegmentFirstPolicyTest.java | 10 +- .../ReindexingConfigBuilderTest.java | 6 +- .../compaction/ReindexingFilterRuleTest.java | 52 +++- .../coordination/LoadableDataSegmentTest.java | 31 ++- ...eSchemaDataSourceCompactionConfigTest.java | 32 ++- .../coordinator/duty/CompactSegmentsTest.java | 3 +- .../server/http/DataSegmentPlusTest.java | 20 +- 29 files changed, 746 insertions(+), 144 deletions(-) create mode 100644 indexing-service/src/test/java/org/apache/druid/indexing/common/task/NativeCompactionRunnerTest.java diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/AutoCompactionTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/AutoCompactionTest.java index 4bc72870a1e5..ba8df1b802cd 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/AutoCompactionTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/AutoCompactionTest.java @@ -1528,7 +1528,7 @@ public void testAutoCompactionDutyWithFilter(boolean useSupervisors) throws Exce NO_SKIP_OFFSET, null, null, - new CompactionTransformSpec(new SelectorDimFilter("page", "Striker Eureka", null)), + new CompactionTransformSpec(new SelectorDimFilter("page", "Striker Eureka", null), null), null, false, CompactionEngine.NATIVE diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index e73d4ae181e0..880eb317f41b 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -20,12 +20,16 @@ package org.apache.druid.testing.embedded.compact; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableList; import org.apache.druid.catalog.guice.CatalogClientModule; import org.apache.druid.catalog.guice.CatalogCoordinatorModule; import org.apache.druid.common.utils.IdUtils; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; +import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; +import org.apache.druid.indexer.partitions.PartitionsSpec; import org.apache.druid.indexing.common.task.IndexTask; +import org.apache.druid.indexing.common.task.TaskBuilder; import org.apache.druid.indexing.compact.CascadingReindexingTemplate; import org.apache.druid.indexing.compact.CompactionSupervisorSpec; import org.apache.druid.indexing.overlord.Segments; @@ -37,12 +41,16 @@ import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.query.DruidMetrics; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.query.http.ClientSqlQuery; import org.apache.druid.rpc.UpdateResponse; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingFilterRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; @@ -152,26 +160,8 @@ public void test_ingestDayGranularity_andCompactToMonthGranularity_andCompactToY new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) ) .withTuningConfig( - new UserCompactionTaskQueryTuningConfig( - null, - null, - null, - null, - null, - new DimensionRangePartitionsSpec(null, 5000, List.of("item"), false), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null + createTuningConfigWithPartitionsSpec( + new DimensionRangePartitionsSpec(null, 5000, List.of("item"), false) ) ) .build(); @@ -193,26 +183,8 @@ public void test_ingestDayGranularity_andCompactToMonthGranularity_andCompactToY new UserCompactionTaskGranularityConfig(Granularities.YEAR, null, null) ) .withTuningConfig( - new UserCompactionTaskQueryTuningConfig( - null, - null, - null, - null, - null, - new DimensionRangePartitionsSpec(null, 5000, List.of("item"), false), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null + createTuningConfigWithPartitionsSpec( + new DimensionRangePartitionsSpec(null, 5000, List.of("item"), false) ) ) .build(); @@ -258,26 +230,8 @@ public void test_compaction_withPersistLastCompactionStateFalse_storesOnlyFinger new UserCompactionTaskGranularityConfig(Granularities.MONTH, null, null) ) .withTuningConfig( - new UserCompactionTaskQueryTuningConfig( - null, - null, - null, - null, - null, - new DimensionRangePartitionsSpec(1000, null, List.of("item"), false), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null + createTuningConfigWithPartitionsSpec( + new DimensionRangePartitionsSpec(1000, null, List.of("item"), false) ) ) .build(); @@ -342,34 +296,15 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac "tuningConfigRule", "Use dimension range partitioning with max 1000 rows per segment", Period.days(1), - new UserCompactionTaskQueryTuningConfig( - null, - null, - null, - null, - null, - new DimensionRangePartitionsSpec(1000, null, List.of("item"), false), - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ) + createTuningConfigWithPartitionsSpec(new DimensionRangePartitionsSpec(1000, null, List.of("item"), false)) ); ReindexingFilterRule filterRule = new ReindexingFilterRule( "filterRule", "Drop rows where item is 'hat'", Period.days(7), - new SelectorDimFilter("item", "hat", null) + new SelectorDimFilter("item", "hat", null), + null ); InlineReindexingRuleProvider.Builder ruleProvider = InlineReindexingRuleProvider.builder() @@ -400,6 +335,120 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac verifyEventCountOlderThan(Period.days(7), "item", "hat", 0); } + @Test + public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrectly() + { + // Virtual Collumns on nested data is only supported with MSQ compaction engine right now. + CompactionEngine compactionEngine = CompactionEngine.MSQ; + configureCompaction(compactionEngine); + + String jsonDataWithNestedColumn = + "{\"timestamp\":\"2025-06-01T00:00:00.000Z\",\"item\":\"shirt\",\"value\":105," + + "\"extraInfo\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueB\"}}\n" + + "{\"timestamp\":\"2025-06-02T00:00:00.000Z\",\"item\":\"trousers\",\"value\":210," + + "\"extraInfo\":{\"fieldA\":\"valueC\",\"fieldB\":\"valueD\"}}\n" + + "{\"timestamp\":\"2025-06-03T00:00:00.000Z\",\"item\":\"jeans\",\"value\":150," + + "\"extraInfo\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueE\"}}\n" + + "{\"timestamp\":\"2025-06-04T00:00:00.000Z\",\"item\":\"hat\",\"value\":50," + + "\"extraInfo\":{\"fieldA\":\"valueF\",\"fieldB\":\"valueG\"}}"; + + final TaskBuilder.Index task = TaskBuilder + .ofTypeIndex() + .dataSource(dataSource) + .jsonInputFormat() + .inlineInputSourceWithData(jsonDataWithNestedColumn) + .isoTimestampColumn("timestamp") + .schemaDiscovery() + .segmentGranularity("DAY"); + + cluster.callApi().runTask(task.withId(IdUtils.getRandomId()), overlord); + cluster.callApi().waitForAllSegmentsToBeAvailable(dataSource, coordinator, broker); + + Assertions.assertEquals(4, getTotalRowCount()); + + VirtualColumns virtualColumns = VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "extractedFieldA", + "json_value(extraInfo, '$.fieldA')", + ColumnType.STRING, + TestExprMacroTable.INSTANCE + ) + ) + ); + + ReindexingFilterRule filterRule = new ReindexingFilterRule( + "filterByNestedField", + "Remove rows where extraInfo.fieldA = 'valueA'", + Period.days(7), + new SelectorDimFilter("extractedFieldA", "valueA", null), + virtualColumns + ); + + ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( + "tuningConfigRule", + null, + Period.days(7), + createTuningConfigWithPartitionsSpec(new DynamicPartitionsSpec(null, null)) + ); + + CascadingReindexingTemplate cascadingTemplate = new CascadingReindexingTemplate( + dataSource, + null, + null, + InlineReindexingRuleProvider.builder() + .filterRules(List.of(filterRule)) + .tuningConfigRules(List.of(tuningConfigRule)) + .build(), + compactionEngine, + null + ); + + runCompactionWithSpec(cascadingTemplate); + waitForAllCompactionTasksToFinish(); + + // Verify: Should have 2 rows left (valueA appeared in 2 rows, both filtered out) + Assertions.assertEquals(2, getTotalRowCount()); + + // Verify the correct rows were filtered + verifyNoRowsWithNestedValue("extraInfo", "fieldA", "valueA"); + } + + private int getTotalRowCount() + { + String sql = String.format("SELECT COUNT(*) as cnt FROM \"%s\"", dataSource); + String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); + List> rows = JacksonUtils.readValue( + new DefaultObjectMapper(), + result.getBytes(StandardCharsets.UTF_8), + new TypeReference<>() {} + ); + return ((Number) rows.get(0).get("cnt")).intValue(); + } + + private void verifyNoRowsWithNestedValue(String nestedColumn, String field, String value) + { + String sql = String.format( + "SELECT COUNT(*) as cnt FROM \"%s\" WHERE json_value(%s, '$.%s') = '%s'", + dataSource, + nestedColumn, + field, + value + ); + String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); + List> rows = JacksonUtils.readValue( + new DefaultObjectMapper(), + result.getBytes(StandardCharsets.UTF_8), + new TypeReference<>() {} + ); + Assertions.assertEquals( + 0, + ((Number) rows.get(0).get("cnt")).intValue(), + String.format("Expected no rows where %s.%s = '%s'", nestedColumn, field, value) + ); + } + + private String generateEventsInInterval(Interval interval, int numEvents, long spacingMillis) { List events = new ArrayList<>(); @@ -566,4 +615,30 @@ private void verifyEventCountOlderThan(Period period, String dimension, String v ) ); } + + private UserCompactionTaskQueryTuningConfig createTuningConfigWithPartitionsSpec(PartitionsSpec partitionsSpec) + { + return new UserCompactionTaskQueryTuningConfig( + null, + null, + null, + null, + null, + partitionsSpec, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ); + } + } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/NativeCompactionRunner.java b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/NativeCompactionRunner.java index 89eb331df49f..559cdc1c6403 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/common/task/NativeCompactionRunner.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/common/task/NativeCompactionRunner.java @@ -89,6 +89,14 @@ public CompactionConfigValidationResult validateCompactionTask( Map intervalDataSchemaMap ) { + // Virtual columns in filter rules are not supported by native compaction + if (compactionTask.getTransformSpec() != null + && compactionTask.getTransformSpec().getVirtualColumns() != null + && compactionTask.getTransformSpec().getVirtualColumns().getVirtualColumns().length > 0) { + return CompactionConfigValidationResult.failure( + "Virtual columns in filter rules are not supported by the Native compaction engine. Use MSQ compaction engine instead." + ); + } return CompactionConfigValidationResult.success(); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index e8fdbec3fc1f..d2140a4667db 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -30,6 +30,7 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; +import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; @@ -181,9 +182,16 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() params.getFingerprintMapper() ); + // Filter virtual columns to only include ones referenced by the reduced filter + VirtualColumns reducedVirtualColumns = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + reducedTransformSpecFilter, + config.getTransformSpec().getVirtualColumns() + ); + // Safe cast: we know this is InlineSchemaDataSourceCompactionConfig because we just built it - return ((InlineSchemaDataSourceCompactionConfig) config).toBuilder() - .withTransformSpec(new CompactionTransformSpec(reducedTransformSpecFilter)) + return ((InlineSchemaDataSourceCompactionConfig) config) + .toBuilder() + .withTransformSpec(new CompactionTransformSpec(reducedTransformSpecFilter, reducedVirtualColumns)) .build(); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java index 7209bed7697e..7fee6d044df9 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java @@ -23,6 +23,8 @@ import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.VirtualColumn; +import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.timeline.CompactionState; @@ -122,6 +124,39 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( return new NotDimFilter(new OrDimFilter(new ArrayList<>(unappliedRules))); } + /** + * Filters virtual columns to only include ones referenced by the given filter. + * This removes virtual columns that were used by filter rules that have been optimized away. + * + * @param filter the reduced filter to check for column references + * @param virtualColumns the original set of virtual columns + * @return filtered VirtualColumns with only referenced columns, or null if none are referenced + */ + @Nullable + public static VirtualColumns filterVirtualColumnsForFilter( + @Nullable DimFilter filter, + @Nullable VirtualColumns virtualColumns + ) + { + if (virtualColumns == null || filter == null) { + return null; + } + + // Get the set of columns required by the filter + Set requiredColumns = filter.getRequiredColumns(); + + // Filter virtual columns to only include ones whose output name is required + List referencedColumns = new ArrayList<>(); + for (VirtualColumn vc : virtualColumns.getVirtualColumns()) { + if (requiredColumns.contains(vc.getOutputName())) { + referencedColumns.add(vc); + } + } + + // Return null if no virtual columns are referenced, otherwise create new VirtualColumns + return referencedColumns.isEmpty() ? null : VirtualColumns.create(referencedColumns); + } + /** * Extracts the set of applied filters from a {@link CompactionState}. * diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/ClientCompactionTaskQuerySerdeTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/ClientCompactionTaskQuerySerdeTest.java index 1c04c7b5bd2b..4136d082aa51 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/ClientCompactionTaskQuerySerdeTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/ClientCompactionTaskQuerySerdeTest.java @@ -97,7 +97,7 @@ public class ClientCompactionTaskQuerySerdeTest new ClientCompactionTaskGranularitySpec(Granularities.DAY, Granularities.HOUR, true); private static final AggregatorFactory[] METRICS_SPEC = new AggregatorFactory[] {new CountAggregatorFactory("cnt")}; private static final CompactionTransformSpec CLIENT_COMPACTION_TASK_TRANSFORM_SPEC = - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)); + new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null), null); private static final DynamicPartitionsSpec DYNAMIC_PARTITIONS_SPEC = new DynamicPartitionsSpec(100, 30000L); private static final SegmentsSplitHintSpec SEGMENTS_SPLIT_HINT_SPEC = new SegmentsSplitHintSpec(new HumanReadableBytes(100000L), 10); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java index 53a7259f5bc6..c9fc41a61e82 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java @@ -575,7 +575,7 @@ public void testRunCompactionWithFilterShouldStoreInState() throws Exception final CompactionTask compactionTask = builder .inputSpec(new CompactionIntervalSpec(INTERVAL_TO_INDEX, null)) .tuningConfig(AbstractParallelIndexSupervisorTaskTest.DEFAULT_TUNING_CONFIG_FOR_PARALLEL_INDEXING) - .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim", "a", null))) + .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim", "a", null), null)) .build(); final DataSegmentsWithSchemas dataSegmentsWithSchemas = runTask(compactionTask); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunTest.java index 314db1c98691..f0efad0da8b5 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskRunTest.java @@ -748,7 +748,7 @@ public void testCompactionWithFilterInTransformSpec() throws Exception final CompactionTask compactionTask = compactionTaskBuilder() .interval(Intervals.of("2014-01-01/2014-01-02")) .granularitySpec(new ClientCompactionTaskGranularitySpec(Granularities.DAY, null, null)) - .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim", "a", null))) + .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim", "a", null), null)) .build(); Pair resultPair = runTask(compactionTask); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java index 3ad8acf7c5b1..eb6924466801 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskTest.java @@ -430,7 +430,7 @@ public void testCreateCompactionTaskWithConflictingGranularitySpecAndSegmentGran public void testCreateCompactionTaskWithTransformSpec() { CompactionTransformSpec transformSpec = - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)); + new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null), null); final Builder builder = new Builder( DATA_SOURCE, segmentCacheManagerFactory @@ -1773,7 +1773,7 @@ public void testGetDefaultLookupLoadingSpecWithTransformSpec() ); final CompactionTask task = builder .interval(Intervals.of("2000-01-01/2000-01-02")) - .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null))) + .transformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null), null)) .build(); Assert.assertEquals(LookupLoadingSpec.ALL, task.getLookupLoadingSpec()); } diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/NativeCompactionRunnerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/NativeCompactionRunnerTest.java new file mode 100644 index 000000000000..dcb801ab4ecf --- /dev/null +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/NativeCompactionRunnerTest.java @@ -0,0 +1,118 @@ +/* + * 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.indexing.common.task; + +import com.google.common.collect.ImmutableList; +import org.apache.druid.indexing.common.SegmentCacheManagerFactory; +import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.query.expression.TestExprMacroTable; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.indexing.DataSchema; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; +import org.apache.druid.server.coordinator.CompactionConfigValidationResult; +import org.joda.time.Interval; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Collections; +import java.util.Map; + +public class NativeCompactionRunnerTest +{ + private static final NativeCompactionRunner NATIVE_COMPACTION_RUNNER = new NativeCompactionRunner( + Mockito.mock(SegmentCacheManagerFactory.class) + ); + + @Test + public void testVirtualColumnsInTransformSpecAreNotSupported() + { + VirtualColumns virtualColumns = VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "extractedField", + "json_value(metadata, '$.category')", + ColumnType.STRING, + TestExprMacroTable.INSTANCE + ) + ) + ); + + CompactionTransformSpec transformSpec = new CompactionTransformSpec(null, virtualColumns); + + CompactionTask compactionTask = createCompactionTask(transformSpec); + Map intervalDataschemas = Collections.emptyMap(); + + CompactionConfigValidationResult validationResult = NATIVE_COMPACTION_RUNNER.validateCompactionTask( + compactionTask, + intervalDataschemas + ); + + Assert.assertFalse(validationResult.isValid()); + Assert.assertEquals( + "Virtual columns in filter rules are not supported by the Native compaction engine. Use MSQ compaction engine instead.", + validationResult.getReason() + ); + } + + @Test + public void testNoVirtualColumnsIsValid() + { + CompactionTask compactionTask = createCompactionTask(null); + Map intervalDataschemas = Collections.emptyMap(); + + CompactionConfigValidationResult validationResult = NATIVE_COMPACTION_RUNNER.validateCompactionTask( + compactionTask, + intervalDataschemas + ); + + Assert.assertTrue(validationResult.isValid()); + } + + @Test + public void testEmptyVirtualColumnsIsValid() + { + CompactionTransformSpec transformSpec = new CompactionTransformSpec(null, VirtualColumns.EMPTY); + + CompactionTask compactionTask = createCompactionTask(transformSpec); + Map intervalDataschemas = Collections.emptyMap(); + + CompactionConfigValidationResult validationResult = NATIVE_COMPACTION_RUNNER.validateCompactionTask( + compactionTask, + intervalDataschemas + ); + + Assert.assertTrue(validationResult.isValid()); + } + + private CompactionTask createCompactionTask(CompactionTransformSpec transformSpec) + { + SegmentCacheManagerFactory segmentCacheManagerFactory = Mockito.mock(SegmentCacheManagerFactory.class); + CompactionTask.Builder builder = new CompactionTask.Builder( + "dataSource", + segmentCacheManagerFactory + ); + builder.inputSpec(new CompactionIntervalSpec(Intervals.of("2020-01-01/2020-01-02"), null)); + builder.transformSpec(transformSpec); + return builder.build(); + } +} diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java index fbf72274e662..532b3bb206c6 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java @@ -19,20 +19,25 @@ package org.apache.druid.indexing.compact; +import com.google.common.collect.ImmutableList; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.TestDataSource; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.HeapMemoryIndexingStateStorage; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; @@ -347,7 +352,7 @@ private CompactionState createStateWithFilters(DimFilter... filters) { OrDimFilter orFilter = new OrDimFilter(Arrays.asList(filters)); NotDimFilter notFilter = new NotDimFilter(orFilter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter, null); return new CompactionState( null, @@ -363,7 +368,7 @@ private CompactionState createStateWithFilters(DimFilter... filters) private CompactionState createStateWithSingleFilter(DimFilter filter) { NotDimFilter notFilter = new NotDimFilter(filter); - CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter); + CompactionTransformSpec transformSpec = new CompactionTransformSpec(notFilter, null); return new CompactionState( null, @@ -389,6 +394,92 @@ private CompactionState createStateWithoutFilters() ); } + @Test + public void testFilterVirtualColumnsForFilter_someColumnsReferenced() + { + // Create virtual columns + VirtualColumns virtualColumns = VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "vc1", + "col1 + 1", + ColumnType.LONG, + TestExprMacroTable.INSTANCE + ), + new ExpressionVirtualColumn( + "vc2", + "col2 + 2", + ColumnType.LONG, + TestExprMacroTable.INSTANCE + ), + new ExpressionVirtualColumn( + "vc3", + "col3 + 3", + ColumnType.LONG, + TestExprMacroTable.INSTANCE + ) + ) + ); + + // Create a filter that only references vc1 and vc3 + DimFilter filter = new OrDimFilter( + Arrays.asList( + new SelectorDimFilter("vc1", "value1", null), + new SelectorDimFilter("vc3", "value3", null) + ) + ); + + VirtualColumns filtered = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + filter, + virtualColumns + ); + + Assert.assertNotNull(filtered); + Assert.assertEquals(2, filtered.getVirtualColumns().length); + + Set outputNames = new HashSet<>(); + for (org.apache.druid.segment.VirtualColumn vc : filtered.getVirtualColumns()) { + outputNames.add(vc.getOutputName()); + } + + Assert.assertTrue(outputNames.contains("vc1")); + Assert.assertFalse(outputNames.contains("vc2")); // vc2 should be filtered out + Assert.assertTrue(outputNames.contains("vc3")); + } + + @Test + public void testFilterVirtualColumnsForFilter_noColumnsReferenced() + { + // Create virtual columns + VirtualColumns virtualColumns = VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "vc1", + "col1 + 1", + ColumnType.LONG, + TestExprMacroTable.INSTANCE + ), + new ExpressionVirtualColumn( + "vc2", + "col2 + 2", + ColumnType.LONG, + TestExprMacroTable.INSTANCE + ) + ) + ); + + // Create a filter that references a physical column, not virtual columns + DimFilter filter = new SelectorDimFilter("regularColumn", "value", null); + + VirtualColumns filtered = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + filter, + virtualColumns + ); + + // Should return null when no virtual columns are referenced so all are filtered out + Assert.assertNull(filtered); + } + /** * Helper to sync the cache with states stored in the manager (for tests that persist states). */ diff --git a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java index db92290b1f99..ee5a55a136eb 100644 --- a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java +++ b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java @@ -73,6 +73,7 @@ import org.apache.druid.segment.column.RowSignature; import org.apache.druid.segment.indexing.CombinedDataSchema; import org.apache.druid.segment.indexing.DataSchema; +import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.coordinator.CompactionConfigValidationResult; import org.apache.druid.server.security.Action; @@ -265,7 +266,7 @@ public List createMsqControllerTasks( Query query; Interval interval = intervalDataSchema.getKey(); DataSchema dataSchema = intervalDataSchema.getValue(); - Map inputColToVirtualCol = getVirtualColumns(dataSchema, interval); + Map inputColToVirtualCol = getVirtualColumns(dataSchema, interval, compactionTask.getTransformSpec()); if (isGroupBy(dataSchema)) { query = buildGroupByQuery(compactionTask, interval, dataSchema, inputColToVirtualCol); @@ -558,7 +559,7 @@ private static boolean isQueryGranularityEmptyOrNone(DataSchema dataSchema) * grouping on them without unnesting. * */ - private Map getVirtualColumns(DataSchema dataSchema, Interval interval) + private Map getVirtualColumns(DataSchema dataSchema, Interval interval, CompactionTransformSpec compactionTransformSpec) { Map inputColToVirtualCol = new HashMap<>(); if (!isQueryGranularityEmptyOrNone(dataSchema)) { @@ -619,6 +620,13 @@ private Map getVirtualColumns(DataSchema dataSchema, Inte ); } } + + if (compactionTransformSpec != null && compactionTransformSpec.getVirtualColumns() != null) { + for (VirtualColumn vc : compactionTransformSpec.getVirtualColumns().getVirtualColumns()) { + inputColToVirtualCol.put(vc.getOutputName(), vc); + } + } + return inputColToVirtualCol; } @@ -638,7 +646,7 @@ private Query buildGroupByQuery( List postAggregators = inputColToVirtualCol.entrySet() .stream() - .filter(entry -> !entry.getKey().equals(ColumnHolder.TIME_COLUMN_NAME)) + .filter(entry -> entry.getKey().startsWith(ARRAY_VIRTUAL_COLUMN_PREFIX)) .map( entry -> new ExpressionPostAggregator( diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java index 705ebc24452b..ee9f2c5ebd7f 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/indexing/MSQCompactionRunnerTest.java @@ -672,7 +672,7 @@ private CompactionTask createCompactionTask( ) { CompactionTransformSpec transformSpec = - new CompactionTransformSpec(dimFilter); + new CompactionTransformSpec(dimFilter, null); final CompactionTask.Builder builder = new CompactionTask.Builder( DATA_SOURCE, null diff --git a/multi-stage-query/src/test/java/org/apache/druid/msq/input/table/DataSegmentWithLocationTest.java b/multi-stage-query/src/test/java/org/apache/druid/msq/input/table/DataSegmentWithLocationTest.java index 1117c137b310..4fef5fd8d3d1 100644 --- a/multi-stage-query/src/test/java/org/apache/druid/msq/input/table/DataSegmentWithLocationTest.java +++ b/multi-stage-query/src/test/java/org/apache/druid/msq/input/table/DataSegmentWithLocationTest.java @@ -21,15 +21,20 @@ import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import org.apache.druid.data.input.impl.DimensionsSpec; import org.apache.druid.indexer.granularity.GranularitySpec; import org.apache.druid.indexer.partitions.HashedPartitionsSpec; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.coordination.DruidServerMetadata; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; @@ -72,7 +77,19 @@ public void testSerde_dataSegmentWithLocation() throws Exception new HashedPartitionsSpec(100000, null, List.of("dim1")), new DimensionsSpec(DimensionsSpec.getDefaultSchemas(List.of("dim1", "bar", "foo"))), List.of(new CountAggregatorFactory("count")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(Map.of(), IndexSpec.class), MAPPER.convertValue(Map.of(), GranularitySpec.class), null diff --git a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java index ac62a6f4e43e..971a48e12049 100644 --- a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java +++ b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.segment.VirtualColumns; import javax.annotation.Nullable; import java.util.Objects; @@ -46,17 +47,20 @@ public static CompactionTransformSpec of(@Nullable TransformSpec transformSpec) return null; } - return new CompactionTransformSpec(transformSpec.getFilter()); + return new CompactionTransformSpec(transformSpec.getFilter(), null); } @Nullable private final DimFilter filter; + @Nullable private final VirtualColumns virtualColumns; @JsonCreator public CompactionTransformSpec( - @JsonProperty("filter") final DimFilter filter + @JsonProperty("filter") final DimFilter filter, + @JsonProperty("virtualColumns") final VirtualColumns virtualColumns ) { this.filter = filter; + this.virtualColumns = virtualColumns; } @JsonProperty @@ -66,6 +70,13 @@ public DimFilter getFilter() return filter; } + @JsonProperty + @Nullable + public VirtualColumns getVirtualColumns() + { + return virtualColumns; + } + @Override public boolean equals(Object o) { @@ -76,13 +87,14 @@ public boolean equals(Object o) return false; } CompactionTransformSpec that = (CompactionTransformSpec) o; - return Objects.equals(filter, that.filter); + return Objects.equals(filter, that.filter) + && Objects.equals(virtualColumns, that.virtualColumns); } @Override public int hashCode() { - return Objects.hash(filter); + return Objects.hash(filter, virtualColumns); } @Override @@ -90,6 +102,7 @@ public String toString() { return "CompactionTransformSpec{" + "filter=" + filter + + ", virtualColumns=" + virtualColumns + '}'; } } diff --git a/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java b/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java index 1d7a09d245f4..e513e52cfeb9 100644 --- a/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java +++ b/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java @@ -19,10 +19,17 @@ package org.apache.druid.segment.transform; +import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import nl.jqno.equalsverifier.EqualsVerifier; import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.math.expr.ExprMacroTable; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.junit.Assert; import org.junit.Test; @@ -43,9 +50,22 @@ public void testEquals() public void testSerde() throws IOException { final CompactionTransformSpec expected = new CompactionTransformSpec( - new SelectorDimFilter("dim1", "foo", null) + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) ); final ObjectMapper mapper = new DefaultObjectMapper(); + mapper.setInjectableValues( + new InjectableValues.Std().addValue(ExprMacroTable.class, TestExprMacroTable.INSTANCE) + ); final byte[] json = mapper.writeValueAsBytes(expected); final CompactionTransformSpec fromJson = (CompactionTransformSpec) mapper.readValue( json, diff --git a/processing/src/test/java/org/apache/druid/timeline/DataSegmentTest.java b/processing/src/test/java/org/apache/druid/timeline/DataSegmentTest.java index 23e88564c8f3..37c175b57f27 100644 --- a/processing/src/test/java/org/apache/druid/timeline/DataSegmentTest.java +++ b/processing/src/test/java/org/apache/druid/timeline/DataSegmentTest.java @@ -32,11 +32,16 @@ import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.jackson.JacksonUtils; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.timeline.DataSegment.PruneSpecsHolder; import org.apache.druid.timeline.partition.NoneShardSpec; import org.apache.druid.timeline.partition.NumberedShardSpec; @@ -66,6 +71,7 @@ public void setUp() { InjectableValues.Std injectableValues = new InjectableValues.Std(); injectableValues.addValue(PruneSpecsHolder.class, PruneSpecsHolder.DEFAULT); + injectableValues.addValue(ExprMacroTable.class, TestExprMacroTable.INSTANCE); MAPPER.setInjectableValues(injectableValues); } @@ -82,7 +88,19 @@ public void testSerializationWithLatestFormat() throws Exception new HashedPartitionsSpec(100000, null, ImmutableList.of("dim1")), new DimensionsSpec(DimensionsSpec.getDefaultSchemas(ImmutableList.of("dim1", "bar", "foo"))), ImmutableList.of(new CountAggregatorFactory("count")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(ImmutableMap.of(), IndexSpec.class), MAPPER.convertValue(ImmutableMap.of(), GranularitySpec.class), null @@ -147,7 +165,19 @@ public void testV1Serialization() throws Exception DimensionsSpec.getDefaultSchemas(ImmutableList.of("dim1", "bar", "foo")) ), ImmutableList.of(new CountAggregatorFactory("count")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(ImmutableMap.of(), IndexSpec.class), MAPPER.convertValue(ImmutableMap.of(), GranularitySpec.class), null @@ -347,7 +377,19 @@ public void testWithLastCompactionState() new DynamicPartitionsSpec(null, null), new DimensionsSpec(DimensionsSpec.getDefaultSchemas(ImmutableList.of("bar", "foo"))), ImmutableList.of(new CountAggregatorFactory("count")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(Map.of("test", "map"), IndexSpec.class), MAPPER.convertValue(Map.of("test2", "map2"), GranularitySpec.class), null @@ -377,7 +419,17 @@ public void testAnnotateWithLastCompactionState() DimensionsSpec dimensionsSpec = new DimensionsSpec(DimensionsSpec.getDefaultSchemas(List.of("bar", "foo"))); List metricsSpec = ImmutableList.of(new CountAggregatorFactory("count")); CompactionTransformSpec transformSpec = new CompactionTransformSpec( - new SelectorDimFilter("dim1", "foo", null) + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) ); IndexSpec indexSpec = MAPPER.convertValue(Map.of("test", "map"), IndexSpec.class).getEffectiveSpec(); GranularitySpec granularitySpec = MAPPER.convertValue(Map.of("test2", "map"), GranularitySpec.class); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index 5a4f3a0a1029..434a113bf9c4 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -24,12 +24,16 @@ import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; +import org.apache.druid.segment.VirtualColumn; +import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.joda.time.DateTime; import org.joda.time.Interval; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -147,17 +151,30 @@ private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder buil return 0; } - // Combine: OR all filters together, wrap in NOT - List removeConditions = rules.stream() - .map(ReindexingFilterRule::getFilter) - .collect(Collectors.toList()); + // Collect filters and virtual columns in a single pass + List removeConditions = new ArrayList<>(); + List allVirtualColumns = new ArrayList<>(); + for (ReindexingFilterRule rule : rules) { + removeConditions.add(rule.getFilter()); + + if (rule.getVirtualColumns() != null) { + allVirtualColumns.addAll(Arrays.asList(rule.getVirtualColumns().getVirtualColumns())); + } + } + + // Combine filters: OR all filters together, wrap in NOT DimFilter removeFilter = removeConditions.size() == 1 ? removeConditions.get(0) : new OrDimFilter(removeConditions); - DimFilter finalFilter = new NotDimFilter(removeFilter); - builder.withTransformSpec(new CompactionTransformSpec(finalFilter)); + + // Create VirtualColumns if any exist + VirtualColumns virtualColumns = allVirtualColumns.isEmpty() + ? null + : VirtualColumns.create(allVirtualColumns); + + builder.withTransformSpec(new CompactionTransformSpec(finalFilter, virtualColumns)); LOG.debug("Applied [%d] filter rules for interval %s", rules.size(), interval); return rules.size(); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index 7b8fef6dcfbd..7f8031cf6636 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -22,6 +22,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.query.filter.DimFilter; +import org.apache.druid.segment.VirtualColumns; import org.joda.time.Period; import javax.annotation.Nonnull; @@ -55,21 +56,50 @@ * "description": "Remove robot traffic from segments older than 90 days" * } * }
+ *

+ * Virtual column support for filtering on nested fields (MSQ engine only): + *

+ * It is important to note that when using virtual columns in the filter, the virtual columns must be defined + * with unique names. Users will have to take care to ensure a rule always has the same unique virtual column names + * to not impact the fingerprinting of segments reindexed with the rule. + *

{@code
+ * {
+ *   "id": "remove-using-nested-field-filter",
+ *   "period": "P90D",
+ *   "filter": {
+ *     "type": "selector",
+ *     "dimension": "extractedField",
+ *     "value": "unwantedValue"
+ *   },
+ *   "virtualColumns": [
+ *     {
+ *       "type": "expression",
+ *       "name": "extractedField",
+ *       "expression": "json_value(metadata, '$.category')",
+ *       "outputType": "STRING"
+ *     }
+ *   ],
+ *   "description": "Remove rows where metadata.category = 'unwantedValue' from segments older than 90 days"
+ * }
+ * }
*/ public class ReindexingFilterRule extends AbstractReindexingRule { private final DimFilter filter; + private final VirtualColumns virtualColumns; @JsonCreator public ReindexingFilterRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("period") @Nonnull Period period, - @JsonProperty("filter") @Nonnull DimFilter filter + @JsonProperty("filter") @Nonnull DimFilter filter, + @JsonProperty("virtualColumns") @Nullable VirtualColumns virtualColumns ) { super(id, description, period); this.filter = Objects.requireNonNull(filter, "filter cannot be null"); + this.virtualColumns = virtualColumns; } @JsonProperty @@ -77,4 +107,11 @@ public DimFilter getFilter() { return filter; } + + @JsonProperty + @Nullable + public VirtualColumns getVirtualColumns() + { + return virtualColumns; + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java index 3db32229e556..e8266a11d23d 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java @@ -34,7 +34,8 @@ public void test_constructor_positiveMonthsNegativeDays_throwsException() "test-rule", null, period, - new SelectorDimFilter("dim", "val", null) + new SelectorDimFilter("dim", "val", null), + null ); } @@ -47,7 +48,8 @@ public void test_constructor_positiveYearsNegativeMonths_throwsException() "test-rule", null, period, - new SelectorDimFilter("dim", "val", null) + new SelectorDimFilter("dim", "val", null), + null ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index d505c84e371a..80ef7e26bdbd 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -515,7 +515,8 @@ private ReindexingFilterRule createFilterRule(String id, Period period) id, "Test rule", period, - new SelectorDimFilter("test", "value", null) + new SelectorDimFilter("test", "value", null), + null ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index c2cf1b287607..d3c283e493ec 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -201,7 +201,7 @@ public void test_getType_returnsInline() private ReindexingFilterRule createFilterRule(String id, Period period) { - return new ReindexingFilterRule(id, null, period, new SelectorDimFilter("dim", "val", null)); + return new ReindexingFilterRule(id, null, period, new SelectorDimFilter("dim", "val", null), null); } private ReindexingMetricsRule createMetricsRule(String id, Period period) diff --git a/server/src/test/java/org/apache/druid/server/compaction/NewestSegmentFirstPolicyTest.java b/server/src/test/java/org/apache/druid/server/compaction/NewestSegmentFirstPolicyTest.java index 4e86abcd2468..ee2a442d2921 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/NewestSegmentFirstPolicyTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/NewestSegmentFirstPolicyTest.java @@ -1431,7 +1431,7 @@ public void testIteratorReturnsSegmentsAsSegmentsWasCompactedAndHaveDifferentFil partitionsSpec, null, null, - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null), null), indexSpec, null, null @@ -1445,7 +1445,7 @@ public void testIteratorReturnsSegmentsAsSegmentsWasCompactedAndHaveDifferentFil partitionsSpec, null, null, - new CompactionTransformSpec(new SelectorDimFilter("dim1", "bar", null)), + new CompactionTransformSpec(new SelectorDimFilter("dim1", "bar", null), null), indexSpec, null, null @@ -1459,7 +1459,7 @@ public void testIteratorReturnsSegmentsAsSegmentsWasCompactedAndHaveDifferentFil partitionsSpec, null, null, - new CompactionTransformSpec(null), + new CompactionTransformSpec(null, null), indexSpec, null, null @@ -1474,7 +1474,7 @@ public void testIteratorReturnsSegmentsAsSegmentsWasCompactedAndHaveDifferentFil // Auto compaction config sets filter=SelectorDimFilter("dim1", "bar", null) CompactionSegmentIterator iterator = createIterator( configBuilder().withTransformSpec( - new CompactionTransformSpec(new SelectorDimFilter("dim1", "bar", null)) + new CompactionTransformSpec(new SelectorDimFilter("dim1", "bar", null), null) ).build(), timeline ); @@ -1518,7 +1518,7 @@ public void testIteratorReturnsSegmentsAsSegmentsWasCompactedAndHaveDifferentFil // Auto compaction config sets filter=null iterator = createIterator( configBuilder().withTransformSpec( - new CompactionTransformSpec(null) + new CompactionTransformSpec(null, null) ).build(), timeline ); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 5e0cda0e8811..70542fdcc158 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -187,14 +187,16 @@ private ReindexingRuleProvider createFullyPopulatedProvider() "filter-30d", null, Period.days(30), - new SelectorDimFilter("country", "US", null) + new SelectorDimFilter("country", "US", null), + null ); ReindexingFilterRule filterRule2 = new ReindexingFilterRule( "filter-60d", null, Period.days(60), - new SelectorDimFilter("device", "mobile", null) + new SelectorDimFilter("device", "mobile", null), + null ); return InlineReindexingRuleProvider.builder() diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index c2d88d75e70a..8df1fb234d3d 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -19,10 +19,15 @@ package org.apache.druid.server.compaction; +import com.google.common.collect.ImmutableList; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -35,11 +40,24 @@ public class ReindexingFilterRuleTest private static final Period PERIOD_30_DAYS = Period.days(30); private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); + private final VirtualColumns virtualColumns = VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ); + + private final ReindexingFilterRule rule = new ReindexingFilterRule( "test-filter-rule", "Remove robot traffic", PERIOD_30_DAYS, - testFilter + testFilter, + virtualColumns ); @Test @@ -122,7 +140,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule(null, "description", PERIOD_30_DAYS, testFilter) + () -> new ReindexingFilterRule(null, "description", PERIOD_30_DAYS, testFilter, null) ); } @@ -131,7 +149,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule("test-id", "description", null, testFilter) + () -> new ReindexingFilterRule("test-id", "description", null, testFilter, null) ); } @@ -141,7 +159,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter, null) ); } @@ -151,7 +169,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-30); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter, null) ); } @@ -160,7 +178,7 @@ public void test_constructor_nullFilter_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule("test-id", "description", PERIOD_30_DAYS, null) + () -> new ReindexingFilterRule("test-id", "description", PERIOD_30_DAYS, null, null) ); } @@ -175,7 +193,8 @@ public void test_constructor_periodWithMonths_succeeds() "test-id", "6 month rule", period, - testFilter + testFilter, + null ); Assert.assertEquals(period, rule.getPeriod()); @@ -190,7 +209,8 @@ public void test_constructor_periodWithYears_succeeds() "test-id", "1 year rule", period, - testFilter + testFilter, + null ); Assert.assertEquals(period, rule.getPeriod()); @@ -205,7 +225,8 @@ public void test_constructor_periodWithMixedMonthsAndDays_succeeds() "test-id", "6 months 15 days rule", period, - testFilter + testFilter, + null ); Assert.assertEquals(period, rule.getPeriod()); @@ -220,7 +241,8 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds() "test-id", "1 year 3 months 10 days rule", period, - testFilter + testFilter, + null ); Assert.assertEquals(period, rule.getPeriod()); @@ -233,7 +255,7 @@ public void test_constructor_zeroMonthsPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.months(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter, null) ); } @@ -244,7 +266,7 @@ public void test_constructor_negativeMonthsPeriod_throwsIllegalArgumentException Period negativePeriod = Period.months(-6); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter) + () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter, null) ); } @@ -261,7 +283,8 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() "test-month-rule", "6 months rule", sixMonths, - testFilter + testFilter, + null ); // Interval ending before 6-month threshold - should be FULL @@ -299,7 +322,8 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() "test-year-rule", "1 year rule", oneYear, - testFilter + testFilter, + null ); // Interval ending before 1-year threshold - should be FULL diff --git a/server/src/test/java/org/apache/druid/server/coordination/LoadableDataSegmentTest.java b/server/src/test/java/org/apache/druid/server/coordination/LoadableDataSegmentTest.java index 62dda2cd694b..21e5626cae72 100644 --- a/server/src/test/java/org/apache/druid/server/coordination/LoadableDataSegmentTest.java +++ b/server/src/test/java/org/apache/druid/server/coordination/LoadableDataSegmentTest.java @@ -19,16 +19,23 @@ package org.apache.druid.server.coordination; +import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import org.apache.druid.data.input.impl.DimensionsSpec; import org.apache.druid.indexer.granularity.GranularitySpec; import org.apache.druid.indexer.partitions.HashedPartitionsSpec; import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.Intervals; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; @@ -44,7 +51,15 @@ public class LoadableDataSegmentTest { - private static final ObjectMapper MAPPER = new DefaultObjectMapper(); + private static final ObjectMapper MAPPER; + + static { + MAPPER = new DefaultObjectMapper(); + MAPPER.setInjectableValues( + new InjectableValues.Std().addValue(ExprMacroTable.class, TestExprMacroTable.INSTANCE) + ); + } + private static final int TEST_VERSION = 0x9; @Test @@ -59,7 +74,19 @@ public void testSerde_LoadableDataSegment() throws Exception new HashedPartitionsSpec(100000, null, List.of("dim1")), new DimensionsSpec(DimensionsSpec.getDefaultSchemas(List.of("dim1", "bar", "foo"))), List.of(new CountAggregatorFactory("count")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(Map.of(), IndexSpec.class), MAPPER.convertValue(Map.of(), GranularitySpec.class), null diff --git a/server/src/test/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfigTest.java b/server/src/test/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfigTest.java index 85ce0bfef017..4c58d551509d 100644 --- a/server/src/test/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfigTest.java +++ b/server/src/test/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfigTest.java @@ -19,6 +19,7 @@ package org.apache.druid.server.coordinator; +import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -29,14 +30,19 @@ import org.apache.druid.jackson.DefaultObjectMapper; import org.apache.druid.java.util.common.HumanReadableBytes; import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.data.CompressionFactory.LongEncodingStrategy; import org.apache.druid.segment.data.CompressionStrategy; import org.apache.druid.segment.incremental.OnheapIncrementalIndex; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.segment.writeout.TmpFileSegmentWriteOutMediumFactory; import org.apache.druid.testing.InitializedNullHandlingTest; import org.joda.time.Duration; @@ -48,7 +54,15 @@ public class InlineSchemaDataSourceCompactionConfigTest extends InitializedNullHandlingTest { - private static final ObjectMapper OBJECT_MAPPER = new DefaultObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER; + + static { + OBJECT_MAPPER = new DefaultObjectMapper(); + OBJECT_MAPPER.setInjectableValues( + new InjectableValues.Std().addValue(ExprMacroTable.class, TestExprMacroTable.INSTANCE) + ); + } + @Test public void testSerdeBasic() throws IOException @@ -436,7 +450,21 @@ public void testSerdeTransformSpec() throws IOException .forDataSource("dataSource") .withInputSegmentSizeBytes(500L) .withSkipOffsetFromLatest(new Period(3600)) - .withTransformSpec(new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null))) + .withTransformSpec( + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ) + ) .withTaskContext(ImmutableMap.of("key", "val")) .build(); final String json = OBJECT_MAPPER.writeValueAsString(config); diff --git a/server/src/test/java/org/apache/druid/server/coordinator/duty/CompactSegmentsTest.java b/server/src/test/java/org/apache/druid/server/coordinator/duty/CompactSegmentsTest.java index cd92e8f1999a..2983907762db 100644 --- a/server/src/test/java/org/apache/druid/server/coordinator/duty/CompactSegmentsTest.java +++ b/server/src/test/java/org/apache/druid/server/coordinator/duty/CompactSegmentsTest.java @@ -1285,7 +1285,8 @@ public void testCompactWithTransformSpec() .withTuningConfig(getTuningConfig(3)) .withTransformSpec( new CompactionTransformSpec( - new SelectorDimFilter("dim1", "foo", null) + new SelectorDimFilter("dim1", "foo", null), + null ) ) .withEngine(engine) diff --git a/server/src/test/java/org/apache/druid/server/http/DataSegmentPlusTest.java b/server/src/test/java/org/apache/druid/server/http/DataSegmentPlusTest.java index c86a64bb077b..84d8021700aa 100644 --- a/server/src/test/java/org/apache/druid/server/http/DataSegmentPlusTest.java +++ b/server/src/test/java/org/apache/druid/server/http/DataSegmentPlusTest.java @@ -32,10 +32,15 @@ import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.jackson.JacksonUtils; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.expression.TestExprMacroTable; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.IndexSpec; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; @@ -58,6 +63,7 @@ public class DataSegmentPlusTest public void setUp() { InjectableValues.Std injectableValues = new InjectableValues.Std(); + injectableValues.addValue(ExprMacroTable.class, TestExprMacroTable.INSTANCE); injectableValues.addValue(DataSegment.PruneSpecsHolder.class, DataSegment.PruneSpecsHolder.DEFAULT); MAPPER.setInjectableValues(injectableValues); } @@ -95,7 +101,19 @@ public void testSerde() throws JsonProcessingException DimensionsSpec.getDefaultSchemas(ImmutableList.of("dim1", "bar", "foo")) ), ImmutableList.of(new CountAggregatorFactory("cnt")), - new CompactionTransformSpec(new SelectorDimFilter("dim1", "foo", null)), + new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.create( + ImmutableList.of( + new ExpressionVirtualColumn( + "isRobotFiltered", + "concat(isRobot, '_filtered')", + ColumnType.STRING, + ExprMacroTable.nil() + ) + ) + ) + ), MAPPER.convertValue(ImmutableMap.of(), IndexSpec.class), MAPPER.convertValue(ImmutableMap.of(), GranularitySpec.class), null From 77fea055cd9916d645ea883f28d9d8ff3980aeb5 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 26 Jan 2026 19:23:59 -0600 Subject: [PATCH 28/90] fix forbidden misses --- .../testing/embedded/compact/CompactionSupervisorTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 880eb317f41b..1d4b555577cf 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -416,7 +416,7 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec private int getTotalRowCount() { - String sql = String.format("SELECT COUNT(*) as cnt FROM \"%s\"", dataSource); + String sql = StringUtils.format("SELECT COUNT(*) as cnt FROM \"%s\"", dataSource); String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); List> rows = JacksonUtils.readValue( new DefaultObjectMapper(), @@ -428,7 +428,7 @@ private int getTotalRowCount() private void verifyNoRowsWithNestedValue(String nestedColumn, String field, String value) { - String sql = String.format( + String sql = StringUtils.format( "SELECT COUNT(*) as cnt FROM \"%s\" WHERE json_value(%s, '$.%s') = '%s'", dataSource, nestedColumn, @@ -444,7 +444,7 @@ private void verifyNoRowsWithNestedValue(String nestedColumn, String field, Stri Assertions.assertEquals( 0, ((Number) rows.get(0).get("cnt")).intValue(), - String.format("Expected no rows where %s.%s = '%s'", nestedColumn, field, value) + StringUtils.format("Expected no rows where %s.%s = '%s'", nestedColumn, field, value) ); } From a39c7d35348344a990c61433c99f9464bd331c30 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 27 Jan 2026 08:06:47 -0600 Subject: [PATCH 29/90] Wait or data availability before querying more than segment counts --- .../testing/embedded/compact/CompactionSupervisorTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 1d4b555577cf..b015a65d71ad 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -407,6 +407,8 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec runCompactionWithSpec(cascadingTemplate); waitForAllCompactionTasksToFinish(); + cluster.callApi().waitForAllSegmentsToBeAvailable(dataSource, coordinator, broker); + // Verify: Should have 2 rows left (valueA appeared in 2 rows, both filtered out) Assertions.assertEquals(2, getTotalRowCount()); From 816c7f9b900c24e4f566e5421dc077b94d94bff8 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 27 Jan 2026 18:01:29 -0600 Subject: [PATCH 30/90] Try to clear up confusion by renaming period to olderThan to signify what date ranges rules apply to --- .../compaction/AbstractReindexingRule.java | 20 ++++++------- .../InlineReindexingRuleProvider.java | 4 +-- .../compaction/ReindexingDimensionsRule.java | 21 ++++++-------- .../compaction/ReindexingFilterRule.java | 29 +++++++++---------- .../compaction/ReindexingGranularityRule.java | 26 ++++++++--------- .../compaction/ReindexingIOConfigRule.java | 13 ++++----- .../compaction/ReindexingMetricsRule.java | 19 +++++------- .../compaction/ReindexingProjectionRule.java | 18 +++++------- .../server/compaction/ReindexingRule.java | 6 ++-- .../ReindexingTuningConfigRule.java | 23 +++++++-------- .../ReindexingDimensionsRuleTest.java | 4 +-- .../compaction/ReindexingFilterRuleTest.java | 12 ++++---- .../ReindexingGranularityRuleTest.java | 4 +-- .../ReindexingIOConfigRuleTest.java | 4 +-- .../compaction/ReindexingMetricsRuleTest.java | 4 +-- .../ReindexingProjectionRuleTest.java | 4 +-- .../ReindexingTuningConfigRuleTest.java | 4 +-- 17 files changed, 99 insertions(+), 116 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index 72255c774ae5..a3b0eaf61a67 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -33,9 +33,9 @@ /** * Base implementation for reindexing rules that apply based on data age thresholds. *

- * Provides period-based applicability logic: a rule with period P7D applies to data - * older than 7 days. Subclasses define specific reindexing configuration (granularity, - * filters, tuning, etc.) and whether multiple rules can combine (additive vs non-additive). + * Provides period-based applicability logic: a rule with {@link AbstractReindexingRule#olderThan} of P7D applies to + * data older than 7 days as compared to the time of evaluation. Subclasses define specific reindexing configuration + * (granularity, filters, tuning, etc.) and whether multiple rules can combine (additive vs non-additive). *

* The {@link #appliesTo(Interval, DateTime)} method determines if an interval is fully, * partially, or not covered by this rule's threshold, enabling cascading reindexing @@ -47,19 +47,19 @@ public abstract class AbstractReindexingRule implements ReindexingRule private final String id; private final String description; - private final Period period; + private final Period olderThan; public AbstractReindexingRule( @Nonnull String id, @Nullable String description, - @Nonnull Period period + @Nonnull Period olderThan ) { this.id = Objects.requireNonNull(id, "id cannot be null"); this.description = description; - this.period = Objects.requireNonNull(period, "period cannot be null"); + this.olderThan = Objects.requireNonNull(olderThan, "olderThan period cannot be null"); - validatePeriodIsPositive(period); + validatePeriodIsPositive(olderThan); } /** @@ -130,9 +130,9 @@ public String getDescription() @JsonProperty @Override - public Period getPeriod() + public Period getOlderThan() { - return period; + return olderThan; } @Override @@ -142,7 +142,7 @@ public AppliesToMode appliesTo(Interval interval, @Nullable DateTime referenceTi DateTime intervalEnd = interval.getEnd(); DateTime intervalStart = interval.getStart(); - DateTime threshold = now.minus(period); + DateTime threshold = now.minus(olderThan); if (intervalEnd.isBefore(threshold) || intervalEnd.isEqual(threshold)) { LOG.debug("Reindexing rule [%s] applies FULLY to interval [%s]. Threshold: [%s]", id, interval, threshold); diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index 4a836e254661..70f2c29e415e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -193,7 +193,7 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) reindexingTuningConfigRules ) .flatMap(List::stream) - .map(ReindexingRule::getPeriod) + .map(ReindexingRule::getOlderThan) .distinct() .sorted(Comparator.comparingLong(period -> { DateTime endTime = referenceTime.plus(period); @@ -290,7 +290,7 @@ private T getApplicableRule(List rules, Interval i return Collections.min( applicableRules, Comparator.comparingLong(r -> { - DateTime threshold = referenceTime.minus(r.getPeriod()); + DateTime threshold = referenceTime.minus(r.getOlderThan()); return threshold.getMillis(); }) ); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java index cce4df21007c..64c505b19f14 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java @@ -29,22 +29,19 @@ import java.util.Objects; /** - * A compaction dimensions rule that specifies dimension schema for segments older than a specified period. + * A {@link ReindexingRule} that specifies a {@link UserCompactionTaskDimensionsConfig} for tasks to configure. *

- * This rule defines which dimensions to include in compacted segments and their types. For example, - * dropping unused dimensions from older data can reduce storage size and improve query performance. + * This rule defines which dimensions and their types for reindexed segments. For example, + * dropping unused dimensions from older data can reduce storage size. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply - * to any segment where the segment's end time is before ("now" - 90 days). - *

- * This is a non-additive rule. Multiple dimensions rules cannot be applied to the same interval safely, + * This is a non-additive rule. Multiple dimensions rules cannot be applied to the same interval, * as a segment can only have one dimensions specification. *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "optimize-dimensions-90d",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "dimensionsSpec": {
  *     "dimensions": [
  *       "country",
@@ -52,7 +49,7 @@
  *       { "type": "long", "name": "user_id" }
  *     ]
  *   },
- *   "description": "Optimize dimension schema for data older than 90 days"
+ *   "description": "modify dimension schema for data older than 90 days"
  * }
  * }
*/ @@ -64,11 +61,11 @@ public class ReindexingDimensionsRule extends AbstractReindexingRule public ReindexingDimensionsRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, - @JsonProperty("period") @Nonnull Period period, + @JsonProperty("olderThan") @Nonnull Period olderThan, @JsonProperty("dimensionsSpec") @Nonnull UserCompactionTaskDimensionsConfig dimensionsSpec ) { - super(id, description, period); + super(id, description, olderThan); this.dimensionsSpec = Objects.requireNonNull(dimensionsSpec, "dimensionsSpec cannot be null"); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java index 7f8031cf6636..248ec9167f97 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java @@ -30,30 +30,27 @@ import java.util.Objects; /** - * A compaction filter rule that specifies rows to remove from segments older than a specified period. + * A {@link ReindexingRule} that specifies patterns to match for removing rows during reindexing. *

- * The filter defines rows to REMOVE from compacted segments. For example, a filter - * {@code selector(isRobot=true)} means "remove rows where isRobot=true". The compaction framework - * automatically wraps these filters in NOT logic during processing. + * The filter defines rows to REMOVE from reindexed segments. For example, a filter + * {@code selector(isRobot=true)} with {@link AbstractReindexingRule#olderThan} P90D "remove rows where isRobot=true + * from data older than 90 days". The reindexing framework automatically wraps these filters in NOT logic during + * processing. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply - * to any segment where the segment's end time is before ("now" - 90 days). - *

- * Multiple rules can apply to the same segment. When multiple rules apply, they are combined as - * NOT(A OR B OR C) for optimal bitmap performance, which is equivalent to NOT A AND NOT B AND NOT C - * but uses fewer operations. + * This is an additive rule. Multiple rules can apply to the same segment. When multiple rules apply, they are combined + * as NOT(A OR B OR C) where A, B, and C come from distinct {@link ReindexingFilterRule}s. *

* Example usage: *

{@code
  * {
  *   "id": "remove-robots-90d",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "filter": {
  *     "type": "selector",
  *     "dimension": "isRobot",
  *     "value": "true"
  *   },
- *   "description": "Remove robot traffic from segments older than 90 days"
+ *   "description": "Remove robot traffic from data older than 90 days"
  * }
  * }
*

@@ -62,10 +59,12 @@ * It is important to note that when using virtual columns in the filter, the virtual columns must be defined * with unique names. Users will have to take care to ensure a rule always has the same unique virtual column names * to not impact the fingerprinting of segments reindexed with the rule. + *

+ * Example inline useage with virtual column: *

{@code
  * {
  *   "id": "remove-using-nested-field-filter",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "filter": {
  *     "type": "selector",
  *     "dimension": "extractedField",
@@ -92,12 +91,12 @@ public class ReindexingFilterRule extends AbstractReindexingRule
   public ReindexingFilterRule(
       @JsonProperty("id") @Nonnull String id,
       @JsonProperty("description") @Nullable String description,
-      @JsonProperty("period") @Nonnull Period period,
+      @JsonProperty("olderThan") @Nonnull Period olderThan,
       @JsonProperty("filter") @Nonnull DimFilter filter,
       @JsonProperty("virtualColumns") @Nullable VirtualColumns virtualColumns
   )
   {
-    super(id, description, period);
+    super(id, description, olderThan);
     this.filter = Objects.requireNonNull(filter, "filter cannot be null");
     this.virtualColumns = virtualColumns;
   }
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java
index afd590c34c02..c5ab92a7b8e0 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java
@@ -21,6 +21,7 @@
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig;
 import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig;
 import org.joda.time.Period;
 
@@ -29,28 +30,25 @@
 import java.util.Objects;
 
 /**
- * A compaction granularity rule that specifies segment and query granularity for segments older than a specified period.
+ * A {@link ReindexingRule} that specifies a {@link UserCompactionTaskDimensionsConfig} for tasks to configure.
  * 

- * This rule controls how time-series data is bucketed during compaction. For example, changing from - * 15-minute segments to hourly segments reduces segment count and improves query performance for - * older data that doesn't require fine-grained time resolution. + * This rule controls how time-series data is bucketed during reindexing as well as the granularity of individual rows + * written to segments. For example, changing from 15-minute segments to hourly segments reduces segment count and + * improves query performance for older data that doesn't require fine-grained time resolution. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P7D will apply - * to any segment where the segment's end time is before ("now" - 7 days). + * This is a non-additive rule. Multiple granularity rules cannot be applied to the same interval, + * as a segment can only have one granularity for each of query and segment granularity. *

- * This is a non-additive rule. Multiple granularity rules cannot be applied to the same interval safely, - * as a segment can only have one granularity. - *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "daily-30d",
- *   "period": "P30D",
+ *   "olderThan": "P30D",
  *   "granularityConfig": {
  *     "segmentGranularity": "DAY",
  *     "queryGranularity": "HOUR"
  *   },
- *   "description": "Compact to daily segments for data older than 30 days"
+ *   "description": "Compact to daily segments with hour query granularity for data older than 30 days"
  * }
  * }
*/ @@ -63,10 +61,10 @@ public class ReindexingGranularityRule extends AbstractReindexingRule public ReindexingGranularityRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, - @JsonProperty("period") @Nonnull Period period, + @JsonProperty("olderThan") @Nonnull Period olderThan, @JsonProperty("granularityConfig") @Nonnull UserCompactionTaskGranularityConfig granularityConfig) { - super(id, description, period); + super(id, description, olderThan); this.granularityConfig = Objects.requireNonNull(granularityConfig, "granularityConfig cannot be null"); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java index d72429595c4e..38b65b263723 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java @@ -29,19 +29,16 @@ import java.util.Objects; /** - * A compaction IO config rule that specifies input/output configuration for segments older than a specified period. - *

- * Rules are evaluated at compaction time based on segment age. A rule with period P30D will apply - * to any segment where the segment's end time is before ("now" - 30 days). + * A {@link ReindexingRule} that specifies a {@link UserCompactionTaskIOConfig} for tasks to configure. *

* This is a non-additive rule. Multiple IO config rules cannot be applied to the same interval safely, * as a compaction job can only use one IO configuration. *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "dropExistingFalse-false",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "ioConfig": {
  *     "dropExisting": false
  *   },
@@ -56,11 +53,11 @@ public class ReindexingIOConfigRule extends AbstractReindexingRule
   public ReindexingIOConfigRule(
       @JsonProperty("id") @Nonnull String id,
       @JsonProperty("description") @Nullable String description,
-      @JsonProperty("period") @Nonnull Period period,
+      @JsonProperty("olderThan") @Nonnull Period olderThan,
       @JsonProperty("ioConfig") @Nonnull UserCompactionTaskIOConfig ioConfig
   )
   {
-    super(id, description, period);
+    super(id, description, olderThan);
     this.ioConfig = Objects.requireNonNull(ioConfig, "ioConfig cannot be null");
   }
 
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java
index 8d25ae5c8000..279fd48ae2b1 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java
@@ -29,23 +29,20 @@
 import java.util.Objects;
 
 /**
- * A compaction metrics rule that specifies aggregation metrics for segments older than a specified period.
+ * A {@link ReindexingRule} that specifies a {@link AggregatorFactory[]} for tasks to configure.
  * 

- * This rule defines the metrics specification used during compaction, enabling rollup and pre-aggregation + * This rule defines the metrics specification used during reindexing, enabling rollup and pre-aggregation * of older data. For example, applying sum and count aggregators to historical data can significantly * reduce storage size while preserving queryability for common aggregation queries. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply - * to any segment where the segment's end time is before ("now" - 90 days). + * This is a non-additive rule. Multiple metrics rules cannot be applied to the same interval, as a segment can only + * have one metrics specification. *

- * This is a non-additive rule. Multiple metrics rules cannot be applied to the same interval safely, - * as a segment can only have one metrics specification. - *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "rollup-90d",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "metricsSpec": [
  *     { "type": "count", "name": "count" },
  *     { "type": "longSum", "name": "total_views", "fieldName": "views" }
@@ -62,11 +59,11 @@ public class ReindexingMetricsRule extends AbstractReindexingRule
   public ReindexingMetricsRule(
       @JsonProperty("id") @Nonnull String id,
       @JsonProperty("description") @Nullable String description,
-      @JsonProperty("period") @Nonnull Period period,
+      @JsonProperty("olderThan") @Nonnull Period olderThan,
       @JsonProperty("metricsSpec") @Nonnull AggregatorFactory[] metricsSpec
   )
   {
-    super(id, description, period);
+    super(id, description, olderThan);
     this.metricsSpec = Objects.requireNonNull(metricsSpec, "metricsSpec cannot be null");
   }
 
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java
index 5baa6910c341..c4d09f829768 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java
@@ -30,22 +30,18 @@
 import java.util.Objects;
 
 /**
- * A compaction projection rule that specifies aggregate projections to add to segments older than a specified period.
+ * A {@link ReindexingRule} that specifies {@link AggregateProjectionSpec}s to use while building segments.
  * 

* This rule defines pre-aggregated views of data that can accelerate specific query patterns. Projections are * particularly useful for older data where query patterns are well-understood and storage efficiency is valuable. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P90D will apply - * to any segment where the segment's end time is before ("now" - 90 days). + * This is a non-additive rule. *

- * This is an additive rule. Multiple projection rules can apply to the same interval, and all projections - * are combined into a single list on the compacted segment. - *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "hourly-projection-90d",
- *   "period": "P90D",
+ *   "olderThan": "P90D",
  *   "projections": [
  *     {
  *       "name": "hourly_agg",
@@ -55,7 +51,7 @@
  *       ]
  *     }
  *   ],
- *   "description": "Add hourly aggregation projection for data older than 90 days"
+ *   "description": "create hourly aggregation projection for data older than 90 days"
  * }
  * }
*/ @@ -67,11 +63,11 @@ public class ReindexingProjectionRule extends AbstractReindexingRule public ReindexingProjectionRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, - @JsonProperty("period") @Nonnull Period period, + @JsonProperty("olderThan") @Nonnull Period olderThan, @JsonProperty("projections") @Nonnull List projections ) { - super(id, description, period); + super(id, description, olderThan); this.projections = Objects.requireNonNull(projections, "projections cannot be null"); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java index 43c601beeea1..358b4c27aba1 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java @@ -29,7 +29,9 @@ * Defines a reindexing configuration that applies to data based on age thresholds. *

* Rules encapsulate specific aspects of reindexing (granularity, filters, tuning, etc.) - * and specify when they should apply via a period threshold. + * and specify when they should apply via a {@link Period} which defines the age threshold for applicability. + *

+ * Rules conditionally apply to data "older than" the rules threshold relative to the time of rule evaluation. */ public interface ReindexingRule { @@ -52,7 +54,7 @@ enum AppliesToMode String getDescription(); - Period getPeriod(); + Period getOlderThan(); /** * Check if this rule applies to the given interval. diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java index 52417532c3a7..b27123ef4f2b 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.segment.indexing.TuningConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.Period; @@ -29,23 +30,19 @@ import java.util.Objects; /** - * A compaction tuning config rule that specifies tuning parameters for segments older than a specified period. + * A {@link ReindexingRule} that specifies a {@link TuningConfig} for tasks to configure. *

- * This rule controls partitioning strategy, indexing behavior, and resource usage during compaction. - * For example, applying range partitioning with specific dimensions to older data can optimize - * query performance for common access patterns. + * This rule controls things like partitioning strategy. For example, applying range partitioning over specific + * dimensions to older data can optimize query performance for common access patterns. *

- * Rules are evaluated at compaction time based on segment age. A rule with period P30D will apply - * to any segment where the segment's end time is before ("now" - 30 days). + * This is a non-additive rule. Multiple tuning config rules cannot be applied to the same interval, as a compaction + * job can only use one tuning configuration. *

- * This is a non-additive rule. Multiple tuning config rules cannot be applied to the same interval safely, - * as a compaction job can only use one tuning configuration. - *

- * Example usage: + * Example inline usage: *

{@code
  * {
  *   "id": "range-partition-30d",
- *   "period": "P30D",
+ *   "olderThan": "P30D",
  *   "tuningConfig": {
  *     "partitionsSpec": {
  *       "type": "range",
@@ -65,11 +62,11 @@ public class ReindexingTuningConfigRule extends AbstractReindexingRule
   public ReindexingTuningConfigRule(
       @JsonProperty("id") @Nonnull String id,
       @JsonProperty("description") @Nullable String description,
-      @JsonProperty("period") @Nonnull Period period,
+      @JsonProperty("olderThan") @Nonnull Period olderThan,
       @JsonProperty("tuningConfig") @Nonnull UserCompactionTaskQueryTuningConfig tuningConfig
   )
   {
-    super(id, description, period);
+    super(id, description, olderThan);
     this.tuningConfig = Objects.requireNonNull(tuningConfig, "tuningConfig cannot be null");
   }
 
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java
index cbb3de0f8b78..1c36b1bb86bc 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java
@@ -109,9 +109,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_14_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_14_DAYS, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java
index 8df1fb234d3d..cb05d2f303e5 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java
@@ -130,9 +130,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_30_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_30_DAYS, rule.getOlderThan());
   }
 
   @Test
@@ -197,7 +197,7 @@ public void test_constructor_periodWithMonths_succeeds()
         null
     );
 
-    Assert.assertEquals(period, rule.getPeriod());
+    Assert.assertEquals(period, rule.getOlderThan());
   }
 
   @Test
@@ -213,7 +213,7 @@ public void test_constructor_periodWithYears_succeeds()
         null
     );
 
-    Assert.assertEquals(period, rule.getPeriod());
+    Assert.assertEquals(period, rule.getOlderThan());
   }
 
   @Test
@@ -229,7 +229,7 @@ public void test_constructor_periodWithMixedMonthsAndDays_succeeds()
         null
     );
 
-    Assert.assertEquals(period, rule.getPeriod());
+    Assert.assertEquals(period, rule.getOlderThan());
   }
 
   @Test
@@ -245,7 +245,7 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds()
         null
     );
 
-    Assert.assertEquals(period, rule.getPeriod());
+    Assert.assertEquals(period, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java
index 98cacc2be561..4a54943c3164 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java
@@ -111,9 +111,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_7_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_7_DAYS, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java
index 3846df03b473..3913aeeab85b 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java
@@ -109,9 +109,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_60_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_60_DAYS, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java
index 591b66d80218..0e87f94445ce 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java
@@ -119,9 +119,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_90_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_90_DAYS, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java
index c35a5cca89ba..0f1d4dfe2cf1 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java
@@ -109,9 +109,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_45_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_45_DAYS, rule.getOlderThan());
   }
 
   @Test
diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java
index b88122c1f8ce..7ef907679a60 100644
--- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java
+++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java
@@ -112,9 +112,9 @@ public void test_getDescription_returnsConfiguredDescription()
   }
 
   @Test
-  public void test_getPeriod_returnsConfiguredPeriod()
+  public void test_getOlderThan_returnsConfiguredPeriod()
   {
-    Assert.assertEquals(PERIOD_21_DAYS, rule.getPeriod());
+    Assert.assertEquals(PERIOD_21_DAYS, rule.getOlderThan());
   }
 
   @Test

From 960353e098709c5f907719151970b2e9cd078c0d Mon Sep 17 00:00:00 2001
From: capistrant 
Date: Tue, 27 Jan 2026 18:26:47 -0600
Subject: [PATCH 31/90] Rename filter to deleteWhere in ReindexingFilterRule to
 make it clear what that is for

---
 .../compaction/ReindexingConfigBuilder.java   |  2 +-
 .../compaction/ReindexingFilterRule.java      | 20 +++++++++----------
 .../compaction/ReindexingFilterRuleTest.java  | 15 +++++++++++---
 3 files changed, 23 insertions(+), 14 deletions(-)

diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
index 434a113bf9c4..803ea4a9ab76 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
@@ -156,7 +156,7 @@ private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder buil
     List allVirtualColumns = new ArrayList<>();
 
     for (ReindexingFilterRule rule : rules) {
-      removeConditions.add(rule.getFilter());
+      removeConditions.add(rule.getDeleteWhere());
 
       if (rule.getVirtualColumns() != null) {
         allVirtualColumns.addAll(Arrays.asList(rule.getVirtualColumns().getVirtualColumns()));
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java
index 248ec9167f97..002a6fb9fb0f 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java
@@ -32,9 +32,9 @@
 /**
  * A {@link ReindexingRule} that specifies patterns to match for removing rows during reindexing.
  * 

- * The filter defines rows to REMOVE from reindexed segments. For example, a filter - * {@code selector(isRobot=true)} with {@link AbstractReindexingRule#olderThan} P90D "remove rows where isRobot=true - * from data older than 90 days". The reindexing framework automatically wraps these filters in NOT logic during + * The {@link #deleteWhere} clause defines rows to REMOVE from reindexed segments. For example, a `deleteWhere` {@link DimFilter} + * {@code selector(isRobot=true)} with {@link AbstractReindexingRule#olderThan} P90D will "remove rows where isRobot=true + * from data older than 90 days". The reindexing framework automatically wraps these delete where clauses in NOT logic during * processing. *

* This is an additive rule. Multiple rules can apply to the same segment. When multiple rules apply, they are combined @@ -45,7 +45,7 @@ * { * "id": "remove-robots-90d", * "olderThan": "P90D", - * "filter": { + * "deleteWhere": { * "type": "selector", * "dimension": "isRobot", * "value": "true" @@ -65,7 +65,7 @@ * { * "id": "remove-using-nested-field-filter", * "olderThan": "P90D", - * "filter": { + * "deleteWhere": { * "type": "selector", * "dimension": "extractedField", * "value": "unwantedValue" @@ -84,7 +84,7 @@ */ public class ReindexingFilterRule extends AbstractReindexingRule { - private final DimFilter filter; + private final DimFilter deleteWhere; private final VirtualColumns virtualColumns; @JsonCreator @@ -92,19 +92,19 @@ public ReindexingFilterRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("filter") @Nonnull DimFilter filter, + @JsonProperty("deleteWhere") @Nonnull DimFilter deleteWhere, @JsonProperty("virtualColumns") @Nullable VirtualColumns virtualColumns ) { super(id, description, olderThan); - this.filter = Objects.requireNonNull(filter, "filter cannot be null"); + this.deleteWhere = Objects.requireNonNull(deleteWhere, "deleteWhere cannot be null"); this.virtualColumns = virtualColumns; } @JsonProperty - public DimFilter getFilter() + public DimFilter getDeleteWhere() { - return filter; + return deleteWhere; } @JsonProperty diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java index cb05d2f303e5..93686e2d31de 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java @@ -109,9 +109,18 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() } @Test - public void test_getFilter_returnsConfiguredFilter() + public void test_getVirtualColumns_returnsConfiguredVirtualColumns() { - DimFilter filter = rule.getFilter(); + VirtualColumns vCols = rule.getVirtualColumns(); + + Assert.assertNotNull(vCols); + Assert.assertEquals(virtualColumns, vCols); + } + + @Test + public void test_getDeleteWhere_returnsConfiguredFilter() + { + DimFilter filter = rule.getDeleteWhere(); Assert.assertNotNull(filter); Assert.assertEquals(testFilter, filter); @@ -174,7 +183,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() } @Test - public void test_constructor_nullFilter_throwsNullPointerException() + public void test_constructor_nullDeleteWhere_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, From f275a98be52ff4d00b8a5f7187f0fcf986bf15fb Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 27 Jan 2026 18:47:38 -0600 Subject: [PATCH 32/90] Rename the ReindexingFilterRule to ReindexingDeletionRule to clarify its purpose --- .../compact/CompactionSupervisorTest.java | 14 +++---- .../compact/CascadingReindexingTemplate.java | 4 +- ...a => ReindexingDeletionRuleOptimizer.java} | 24 +++++------ ... ReindexingDeletionRuleOptimizerTest.java} | 26 ++++++------ .../ComposingReindexingRuleProvider.java | 18 ++++---- .../InlineReindexingRuleProvider.java | 42 +++++++++---------- .../compaction/ReindexingConfigBuilder.java | 4 +- ...rRule.java => ReindexingDeletionRule.java} | 6 +-- .../compaction/ReindexingRuleProvider.java | 10 ++--- .../AbstractReindexingRuleTest.java | 4 +- .../ComposingReindexingRuleProviderTest.java | 36 ++++++++-------- .../InlineReindexingRuleProviderTest.java | 36 ++++++++-------- .../ReindexingConfigBuilderTest.java | 6 +-- ...t.java => ReindexingDeletionRuleTest.java} | 30 ++++++------- 14 files changed, 130 insertions(+), 130 deletions(-) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{ReindexingFilterRuleOptimizer.java => ReindexingDeletionRuleOptimizer.java} (87%) rename indexing-service/src/test/java/org/apache/druid/indexing/compact/{ReindexingFilterRuleOptimizerTest.java => ReindexingDeletionRuleOptimizerTest.java} (92%) rename server/src/main/java/org/apache/druid/server/compaction/{ReindexingFilterRule.java => ReindexingDeletionRule.java} (96%) rename server/src/test/java/org/apache/druid/server/compaction/{ReindexingFilterRuleTest.java => ReindexingDeletionRuleTest.java} (89%) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index b015a65d71ad..ac167e8d2434 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -52,7 +52,7 @@ import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; -import org.apache.druid.server.compaction.ReindexingFilterRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; import org.apache.druid.server.compaction.ReindexingGranularityRule; import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; @@ -299,8 +299,8 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac createTuningConfigWithPartitionsSpec(new DimensionRangePartitionsSpec(1000, null, List.of("item"), false)) ); - ReindexingFilterRule filterRule = new ReindexingFilterRule( - "filterRule", + ReindexingDeletionRule deletionRule = new ReindexingDeletionRule( + "deletionRule", "Drop rows where item is 'hat'", Period.days(7), new SelectorDimFilter("item", "hat", null), @@ -310,7 +310,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac InlineReindexingRuleProvider.Builder ruleProvider = InlineReindexingRuleProvider.builder() .granularityRules(List.of(hourRule, dayRule)) .tuningConfigRules(List.of(tuningConfigRule)) - .filterRules(List.of(filterRule)); + .deletionRules(List.of(deletionRule)); if (compactionEngine == CompactionEngine.NATIVE) { ruleProvider = ruleProvider.ioConfigRules( @@ -377,8 +377,8 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec ) ); - ReindexingFilterRule filterRule = new ReindexingFilterRule( - "filterByNestedField", + ReindexingDeletionRule deletionRule = new ReindexingDeletionRule( + "deleteByNestedField", "Remove rows where extraInfo.fieldA = 'valueA'", Period.days(7), new SelectorDimFilter("extractedFieldA", "valueA", null), @@ -397,7 +397,7 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec null, null, InlineReindexingRuleProvider.builder() - .filterRules(List.of(filterRule)) + .deletionRules(List.of(deletionRule)) .tuningConfigRules(List.of(tuningConfigRule)) .build(), compactionEngine, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index d2140a4667db..bf3e75adc610 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -176,14 +176,14 @@ private static ReindexingConfigFinalizer createCascadingFinalizer() if (shouldOptimizeFilterRules(candidate, config)) { // Compute the minimal set of filter rules needed for this candidate - NotDimFilter reducedTransformSpecFilter = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter reducedTransformSpecFilter = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, (NotDimFilter) config.getTransformSpec().getFilter(), params.getFingerprintMapper() ); // Filter virtual columns to only include ones referenced by the reduced filter - VirtualColumns reducedVirtualColumns = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + VirtualColumns reducedVirtualColumns = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( reducedTransformSpecFilter, config.getTransformSpec().getVirtualColumns() ); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java similarity index 87% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java index 7fee6d044df9..823db26b5577 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java @@ -27,6 +27,7 @@ import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.server.compaction.ReindexingDeletionRule; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; @@ -40,20 +41,19 @@ import java.util.stream.Collectors; /** - * Optimization utilities for filter rule application during reindexing - * + * Optimization utilities for applying {@link ReindexingDeletionRule}s during reindexing *

- * When reindexing with {@link org.apache.druid.server.compaction.ReindexingFilterRule}s, it is possible that candidate - * segments have already applied some or all of the filters in previous reindexing runs. Reapplying such filters would - * be wasteful and redundant. This class provides funcionality to optimize the set of filters to be applied by + * When reindexing with {@link ReindexingDeletionRule}s, it is possible that candidate + * segments have already applied some or all of the deletion rules in previous reindexing runs. Reapplying such rules would + * be wasteful and redundant. This class provides funcionality to optimize the set of rules to be applied by * any given reindexing task. */ -public class ReindexingFilterRuleOptimizer +public class ReindexingDeletionRuleOptimizer { - private static final Logger LOG = new Logger(ReindexingFilterRuleOptimizer.class); + private static final Logger LOG = new Logger(ReindexingDeletionRuleOptimizer.class); /** - * Computes the required set of filter rules to be applied for the given {@link CompactionCandidate}. + * Computes the required set of deletion rules to be applied for the given {@link CompactionCandidate}. *

* We only want to apply the rules that have not yet been applied to all segments in the candidate. This reduces * the amount of work the task needs to do while processing rows during reindexing. @@ -61,8 +61,8 @@ public class ReindexingFilterRuleOptimizer * * @param candidateSegments the {@link CompactionCandidate} * @param expectedFilter the expected filter (as a NotDimFilter wrapping an OrDimFilter) - * @param fingerprintMapper the fingerprint mapper to retrieve applied filters from segment fingerprints - * @return the set of unapplied filter rules wrapped in a NotDimFilter, or null if all rules have been applied + * @param fingerprintMapper the fingerprint mapper to retrieve applied rules from segment fingerprints + * @return the set of unapplied deletion rules wrapped in a NotDimFilter, or null if all rules have been applied */ @Nullable public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( @@ -125,8 +125,8 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( } /** - * Filters virtual columns to only include ones referenced by the given filter. - * This removes virtual columns that were used by filter rules that have been optimized away. + * Filters virtual columns to only include ones referenced by the given {@link DimFilter}. + * This removes virtual columns that were used by deletion rules that have been optimized away. * * @param filter the reduced filter to check for column references * @param virtualColumns the original set of virtual columns diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java similarity index 92% rename from indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java rename to indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java index 532b3bb206c6..cf8a967dc4a2 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingFilterRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java @@ -54,7 +54,7 @@ import java.util.Set; import java.util.stream.Collectors; -public class ReindexingFilterRuleOptimizerTest +public class ReindexingDeletionRuleOptimizerTest { private static final DataSegment WIKI_SEGMENT = DataSegment.builder(SegmentId.of(TestDataSource.WIKI, Intervals.of("2013-01-01/PT1H"), "v1", 0)) @@ -84,7 +84,7 @@ public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -103,7 +103,7 @@ public void test_computeRequiredSetOfFilterRulesForCandidate_oneFilter_nothingTo NotDimFilter expectedFilter = new NotDimFilter(filterB); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -126,7 +126,7 @@ public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -149,7 +149,7 @@ public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -177,7 +177,7 @@ public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -210,7 +210,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -242,7 +242,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -268,7 +268,7 @@ public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters( ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -293,7 +293,7 @@ public void testComputeRequiredFilters_TransformSpecWithSingleFilter() ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -317,7 +317,7 @@ public void testComputeRequiredFilters_SegmentsWithNoFingerprints() new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) ); - NotDimFilter result = ReindexingFilterRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( + NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( candidate, expectedFilter, fingerprintMapper @@ -429,7 +429,7 @@ public void testFilterVirtualColumnsForFilter_someColumnsReferenced() ) ); - VirtualColumns filtered = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + VirtualColumns filtered = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( filter, virtualColumns ); @@ -471,7 +471,7 @@ public void testFilterVirtualColumnsForFilter_noColumnsReferenced() // Create a filter that references a physical column, not virtual columns DimFilter filter = new SelectorDimFilter("regularColumn", "value", null); - VirtualColumns filtered = ReindexingFilterRuleOptimizer.filterVirtualColumnsForFilter( + VirtualColumns filtered = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( filter, virtualColumns ); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 335053e7d2e4..04547f14e210 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -58,7 +58,7 @@ * "type": "inline", * "granularityRules": [{ * "id": "recent-data-granularity", - * "period": "P7D", + * "olderThan": "P7D", * "granularity": "HOUR" * }] * }, @@ -66,13 +66,13 @@ * "type": "inline", * "granularityRules": [{ * "id": "default-granularity", - * "period": "P1D", + * "olderThan": "P1D", * "granularity": "DAY" * }], - * "filterRules": [{ + * "deletionRules": [{ * "id": "remove-bots", - * "period": "P30D", - * "filter": { + * "olderThan": "P30D", + * "deleteWhere": { * "type": "selector", * "dimension": "isRobot", * "value": "true" @@ -141,20 +141,20 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) } @Override - public List getFilterRules() + public List getDeletionRules() { return providers.stream() - .map(ReindexingRuleProvider::getFilterRules) + .map(ReindexingRuleProvider::getDeletionRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); } @Override - public List getFilterRules(Interval interval, DateTime referenceTime) + public List getDeletionRules(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getFilterRules(interval, referenceTime)) + .map(p -> p.getDeletionRules(interval, referenceTime)) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index 70f2c29e415e..719b0b53b7ba 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -54,11 +54,11 @@ *

{@code
  * {
  *   "type": "inline",
- *   "reindexingFilterRules": [
+ *   "reindexingDeletionRules": [
  *     {
  *       "id": "remove-bots-90d",
- *       "period": "P90D",
- *       "filter": {
+ *       "olderThan": "P90D",
+ *       "deleteWhere": {
  *         "type": "not",
  *         "field": {
  *           "type": "selector",
@@ -70,8 +70,8 @@
  *     },
  *     {
  *       "id": "remove-low-priority-180d",
- *       "period": "P180D",
- *       "filter": {
+ *       "olderThan": "P180D",
+ *       "deleteWhere": {
  *         "type": "not",
  *         "field": {
  *           "type": "in",
@@ -89,7 +89,7 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider
 {
   public static final String TYPE = "inline";
 
-  private final List reindexingFilterRules;
+  private final List reindexingDeletionRules;
   private final List reindexingMetricsRules;
   private final List reindexingDimensionsRules;
   private final List reindexingIOConfigRules;
@@ -100,7 +100,7 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider
 
   @JsonCreator
   public InlineReindexingRuleProvider(
-      @JsonProperty("reindexingFilterRules") @Nullable List reindexingFilterRules,
+      @JsonProperty("reindexingDeletionRules") @Nullable List reindexingDeletionRules,
       @JsonProperty("reindexingMetricsRules") @Nullable List reindexingMetricsRules,
       @JsonProperty("reindexingDimensionsRules") @Nullable List reindexingDimensionsRules,
       @JsonProperty("reindexingIOConfigRules") @Nullable List reindexingIOConfigRules,
@@ -109,7 +109,7 @@ public InlineReindexingRuleProvider(
       @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules
   )
   {
-    this.reindexingFilterRules = Configs.valueOrDefault(reindexingFilterRules, Collections.emptyList());
+    this.reindexingDeletionRules = Configs.valueOrDefault(reindexingDeletionRules, Collections.emptyList());
     this.reindexingMetricsRules = Configs.valueOrDefault(reindexingMetricsRules, Collections.emptyList());
     this.reindexingDimensionsRules = Configs.valueOrDefault(reindexingDimensionsRules, Collections.emptyList());
     this.reindexingIOConfigRules = Configs.valueOrDefault(reindexingIOConfigRules, Collections.emptyList());
@@ -131,10 +131,10 @@ public String getType()
   }
 
   @Override
-  @JsonProperty("reindexingFilterRules")
-  public List getFilterRules()
+  @JsonProperty("reindexingDeletionRules")
+  public List getDeletionRules()
   {
-    return reindexingFilterRules;
+    return reindexingDeletionRules;
   }
 
   @Override
@@ -184,7 +184,7 @@ public List getTuningConfigRules()
   public List getCondensedAndSortedPeriods(DateTime referenceTime)
   {
     return Stream.of(
-                     reindexingFilterRules,
+                     reindexingDeletionRules,
                      reindexingMetricsRules,
                      reindexingDimensionsRules,
                      reindexingIOConfigRules,
@@ -204,9 +204,9 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime)
   }
 
   @Override
-  public List getFilterRules(Interval interval, DateTime referenceTime)
+  public List getDeletionRules(Interval interval, DateTime referenceTime)
   {
-    return getApplicableRules(reindexingFilterRules, interval, referenceTime);
+    return getApplicableRules(reindexingDeletionRules, interval, referenceTime);
   }
 
   @Override
@@ -306,7 +306,7 @@ public boolean equals(Object o)
       return false;
     }
     InlineReindexingRuleProvider that = (InlineReindexingRuleProvider) o;
-    return Objects.equals(reindexingFilterRules, that.reindexingFilterRules)
+    return Objects.equals(reindexingDeletionRules, that.reindexingDeletionRules)
            && Objects.equals(reindexingMetricsRules, that.reindexingMetricsRules)
            && Objects.equals(reindexingDimensionsRules, that.reindexingDimensionsRules)
            && Objects.equals(reindexingIOConfigRules, that.reindexingIOConfigRules)
@@ -319,7 +319,7 @@ public boolean equals(Object o)
   public int hashCode()
   {
     return Objects.hash(
-        reindexingFilterRules,
+        reindexingDeletionRules,
         reindexingMetricsRules,
         reindexingDimensionsRules,
         reindexingIOConfigRules,
@@ -333,7 +333,7 @@ public int hashCode()
   public String toString()
   {
     return "InlineReindexingRuleProvider{"
-           + "reindexingFilterRules=" + reindexingFilterRules
+           + "reindexingDeletionRules=" + reindexingDeletionRules
            + ", reindexingMetricsRules=" + reindexingMetricsRules
            + ", reindexingDimensionsRules=" + reindexingDimensionsRules
            + ", reindexingIOConfigRules=" + reindexingIOConfigRules
@@ -345,7 +345,7 @@ public String toString()
 
   public static class Builder
   {
-    private List reindexingFilterRules;
+    private List reindexingDeletionRules;
     private List reindexingMetricsRules;
     private List reindexingDimensionsRules;
     private List reindexingIOConfigRules;
@@ -353,9 +353,9 @@ public static class Builder
     private List reindexingGranularityRules;
     private List reindexingTuningConfigRules;
 
-    public Builder filterRules(List reindexingFilterRules)
+    public Builder deletionRules(List reindexingDeletionRules)
     {
-      this.reindexingFilterRules = reindexingFilterRules;
+      this.reindexingDeletionRules = reindexingDeletionRules;
       return this;
     }
 
@@ -398,7 +398,7 @@ public Builder tuningConfigRules(List reindexingTuni
     public InlineReindexingRuleProvider build()
     {
       return new InlineReindexingRuleProvider(
-          reindexingFilterRules,
+          reindexingDeletionRules,
           reindexingMetricsRules,
           reindexingDimensionsRules,
           reindexingIOConfigRules,
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
index 803ea4a9ab76..ee991918ff69 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java
@@ -146,7 +146,7 @@ private int applyProjectionRules(InlineSchemaDataSourceCompactionConfig.Builder
 
   private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder builder)
   {
-    List rules = provider.getFilterRules(interval, referenceTime);
+    List rules = provider.getDeletionRules(interval, referenceTime);
     if (rules.isEmpty()) {
       return 0;
     }
@@ -155,7 +155,7 @@ private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder buil
     List removeConditions = new ArrayList<>();
     List allVirtualColumns = new ArrayList<>();
 
-    for (ReindexingFilterRule rule : rules) {
+    for (ReindexingDeletionRule rule : rules) {
       removeConditions.add(rule.getDeleteWhere());
 
       if (rule.getVirtualColumns() != null) {
diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java
similarity index 96%
rename from server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java
rename to server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java
index 002a6fb9fb0f..af471b4b4c7e 100644
--- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingFilterRule.java
+++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java
@@ -38,7 +38,7 @@
  * processing.
  * 

* This is an additive rule. Multiple rules can apply to the same segment. When multiple rules apply, they are combined - * as NOT(A OR B OR C) where A, B, and C come from distinct {@link ReindexingFilterRule}s. + * as NOT(A OR B OR C) where A, B, and C come from distinct {@link ReindexingDeletionRule}s. *

* Example usage: *

{@code
@@ -82,13 +82,13 @@
  * }
  * }
*/ -public class ReindexingFilterRule extends AbstractReindexingRule +public class ReindexingDeletionRule extends AbstractReindexingRule { private final DimFilter deleteWhere; private final VirtualColumns virtualColumns; @JsonCreator - public ReindexingFilterRule( + public ReindexingDeletionRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("olderThan") @Nonnull Period olderThan, diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index a865b3a8b302..f1f3e4c98de5 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -81,21 +81,21 @@ default boolean isReady() List getCondensedAndSortedPeriods(DateTime referenceTime); /** - * Returns all reindexing filter rules that apply to the given interval. + * Returns all reindexing deletion rules that apply to the given interval. *

* Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. *

* @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingFilterRule} rules that apply to the given interval. + * @return The list of {@link ReindexingDeletionRule} rules that apply to the given interval. */ - List getFilterRules(Interval interval, DateTime referenceTime); + List getDeletionRules(Interval interval, DateTime referenceTime); /** - * Returns ALL reindexing filter rules. + * Returns ALL reindexing deletion rules. */ - List getFilterRules(); + List getDeletionRules(); /** * Returns the matched reindexing metrics rule that applies to the given interval. diff --git a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java index e8266a11d23d..c0e6fb344a8b 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java @@ -30,7 +30,7 @@ public void test_constructor_positiveMonthsNegativeDays_throwsException() { Period period = Period.months(1).withDays(-40); - new ReindexingFilterRule( + new ReindexingDeletionRule( "test-rule", null, period, @@ -44,7 +44,7 @@ public void test_constructor_positiveYearsNegativeMonths_throwsException() { Period period = new Period(1, -13, 0, 0, 0, 0, 0, 0); - new ReindexingFilterRule( + new ReindexingDeletionRule( "test-rule", null, period, diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 80ef7e26bdbd..be52b715b178 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -80,7 +80,7 @@ public void test_constructor_emptyProviderList_succeeds() Assert.assertEquals("composing", composing.getType()); Assert.assertTrue(composing.isReady()); - Assert.assertTrue(composing.getFilterRules().isEmpty()); + Assert.assertTrue(composing.getDeletionRules().isEmpty()); } @@ -121,26 +121,26 @@ public void test_isReady_emptyProviderList_returnsTrue() } @Test - public void test_getFilterRules_compositingBehavior() + public void test_getDeletionRules_compositingBehavior() { testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().filterRules(rules).build(), - ComposingReindexingRuleProvider::getFilterRules, + rules -> InlineReindexingRuleProvider.builder().deletionRules(rules).build(), + ComposingReindexingRuleProvider::getDeletionRules, createFilterRule("rule1", Period.days(7)), createFilterRule("rule2", Period.days(30)), - ReindexingFilterRule::getId + ReindexingDeletionRule::getId ); } @Test - public void test_getFilterRulesWithInterval_compositingBehavior() + public void test_getDeletionRulesWithInterval_compositingBehavior() { testComposingBehaviorForAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().filterRules(rules).build(), - (provider, it) -> provider.getFilterRules(it.interval, it.time), + rules -> InlineReindexingRuleProvider.builder().deletionRules(rules).build(), + (provider, it) -> provider.getDeletionRules(it.interval, it.time), createFilterRule("rule1", Period.days(7)), createFilterRule("rule2", Period.days(30)), - ReindexingFilterRule::getId + ReindexingDeletionRule::getId ); } @@ -159,14 +159,14 @@ public void test_getGranularityRules_compositingBehavior() @Test public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() { - ReindexingFilterRule rule1 = createFilterRule("rule1", Period.days(7)); - ReindexingFilterRule rule2 = createFilterRule("rule2", Period.months(1)); - ReindexingFilterRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period + ReindexingDeletionRule rule1 = createFilterRule("rule1", Period.days(7)); + ReindexingDeletionRule rule2 = createFilterRule("rule2", Period.months(1)); + ReindexingDeletionRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(rule1)).build(); + .deletionRules(ImmutableList.of(rule1)).build(); ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(rule2, rule3)).build(); + .deletionRules(ImmutableList.of(rule2, rule3)).build(); ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( ImmutableList.of(provider1, provider2) @@ -318,7 +318,7 @@ public void test_equals_sameProviders_returnsTrue() { ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder().build(); ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) + .deletionRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) .build(); ComposingReindexingRuleProvider composing1 = new ComposingReindexingRuleProvider( @@ -337,7 +337,7 @@ public void test_equals_differentProviders_returnsFalse() { ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder().build(); ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) + .deletionRules(ImmutableList.of(createFilterRule("rule1", Period.days(30)))) .build(); ComposingReindexingRuleProvider composing1 = new ComposingReindexingRuleProvider( @@ -509,9 +509,9 @@ public boolean isReady() }; } - private ReindexingFilterRule createFilterRule(String id, Period period) + private ReindexingDeletionRule createFilterRule(String id, Period period) { - return new ReindexingFilterRule( + return new ReindexingDeletionRule( id, "Test rule", period, diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index d3c283e493ec..a50c83751932 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -61,8 +61,8 @@ public void test_constructor_nullListsDefaultToEmpty() InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider(null, null, null, null, null, null, null); - Assert.assertNotNull(provider.getFilterRules()); - Assert.assertTrue(provider.getFilterRules().isEmpty()); + Assert.assertNotNull(provider.getDeletionRules()); + Assert.assertTrue(provider.getDeletionRules().isEmpty()); Assert.assertNotNull(provider.getMetricsRules()); Assert.assertTrue(provider.getMetricsRules().isEmpty()); Assert.assertNotNull(provider.getDimensionsRules()); @@ -80,13 +80,13 @@ public void test_constructor_nullListsDefaultToEmpty() @Test public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() { - ReindexingFilterRule filter30d = createFilterRule("f1", Period.days(30)); - ReindexingFilterRule filter60d = createFilterRule("f2", Period.days(60)); + ReindexingDeletionRule filter30d = createFilterRule("f1", Period.days(30)); + ReindexingDeletionRule filter60d = createFilterRule("f2", Period.days(60)); ReindexingGranularityRule gran30d = createGranularityRule("g1", Period.days(30)); // Duplicate P30D ReindexingGranularityRule gran90d = createGranularityRule("g2", Period.days(90)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(filter30d, filter60d)) + .deletionRules(ImmutableList.of(filter30d, filter60d)) .granularityRules(ImmutableList.of(gran30d, gran90d)) .build(); @@ -112,22 +112,22 @@ public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() @Test public void test_additiveRules_allScenarios() { - ReindexingFilterRule rule30d = createFilterRule("filter-30d", Period.days(30)); - ReindexingFilterRule rule60d = createFilterRule("filter-60d", Period.days(60)); - ReindexingFilterRule rule90d = createFilterRule("filter-90d", Period.days(90)); + ReindexingDeletionRule rule30d = createFilterRule("filter-30d", Period.days(30)); + ReindexingDeletionRule rule60d = createFilterRule("filter-60d", Period.days(60)); + ReindexingDeletionRule rule90d = createFilterRule("filter-90d", Period.days(90)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(rule30d, rule60d, rule90d)) + .deletionRules(ImmutableList.of(rule30d, rule60d, rule90d)) .build(); - List noMatch = provider.getFilterRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); + List noMatch = provider.getDeletionRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); Assert.assertTrue("No rules should match interval that's too recent", noMatch.isEmpty()); - List oneMatch = provider.getFilterRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + List oneMatch = provider.getDeletionRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals("Only rule30d should match", 1, oneMatch.size()); Assert.assertEquals("filter-30d", oneMatch.get(0).getId()); - List multiMatch = provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + List multiMatch = provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); Assert.assertEquals("All 3 additive rules should be returned", 3, multiMatch.size()); Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-30d"))); Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-60d"))); @@ -157,7 +157,7 @@ public void test_nonAdditiveRules_allScenarios() @Test public void test_allRuleTypesWireCorrectly_withInterval() { - ReindexingFilterRule filterRule = createFilterRule("filter", Period.days(30)); + ReindexingDeletionRule filterRule = createFilterRule("filter", Period.days(30)); ReindexingMetricsRule metricsRule = createMetricsRule("metrics", Period.days(30)); ReindexingDimensionsRule dimensionsRule = createDimensionsRule("dimensions", Period.days(30)); ReindexingIOConfigRule ioConfigRule = createIOConfigRule("ioconfig", Period.days(30)); @@ -166,7 +166,7 @@ public void test_allRuleTypesWireCorrectly_withInterval() ReindexingTuningConfigRule tuningConfigRule = createTuningConfigRule("tuning", Period.days(30)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .filterRules(ImmutableList.of(filterRule)) + .deletionRules(ImmutableList.of(filterRule)) .metricsRules(ImmutableList.of(metricsRule)) .dimensionsRules(ImmutableList.of(dimensionsRule)) .ioConfigRules(ImmutableList.of(ioConfigRule)) @@ -175,8 +175,8 @@ public void test_allRuleTypesWireCorrectly_withInterval() .tuningConfigRules(ImmutableList.of(tuningConfigRule)) .build(); - Assert.assertEquals(1, provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("filter", provider.getFilterRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals(1, provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assert.assertEquals("filter", provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); Assert.assertEquals("metrics", provider.getMetricsRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); @@ -199,9 +199,9 @@ public void test_getType_returnsInline() Assert.assertEquals("inline", provider.getType()); } - private ReindexingFilterRule createFilterRule(String id, Period period) + private ReindexingDeletionRule createFilterRule(String id, Period period) { - return new ReindexingFilterRule(id, null, period, new SelectorDimFilter("dim", "val", null), null); + return new ReindexingDeletionRule(id, null, period, new SelectorDimFilter("dim", "val", null), null); } private ReindexingMetricsRule createMetricsRule(String id, Period period) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 70542fdcc158..03fd495be188 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -183,7 +183,7 @@ private ReindexingRuleProvider createFullyPopulatedProvider() ); // Two filter rules (additive) - ReindexingFilterRule filterRule1 = new ReindexingFilterRule( + ReindexingDeletionRule filterRule1 = new ReindexingDeletionRule( "filter-30d", null, Period.days(30), @@ -191,7 +191,7 @@ private ReindexingRuleProvider createFullyPopulatedProvider() null ); - ReindexingFilterRule filterRule2 = new ReindexingFilterRule( + ReindexingDeletionRule filterRule2 = new ReindexingDeletionRule( "filter-60d", null, Period.days(60), @@ -206,7 +206,7 @@ private ReindexingRuleProvider createFullyPopulatedProvider() .dimensionsRules(ImmutableList.of(dimensionsRule)) .ioConfigRules(ImmutableList.of(ioConfigRule)) .projectionRules(ImmutableList.of(projectionRule1, projectionRule2)) - .filterRules(ImmutableList.of(filterRule1, filterRule2)) + .deletionRules(ImmutableList.of(filterRule1, filterRule2)) .build(); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java similarity index 89% rename from server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java index 93686e2d31de..6af4b8f07e16 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingFilterRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java @@ -34,7 +34,7 @@ import org.junit.Assert; import org.junit.Test; -public class ReindexingFilterRuleTest +public class ReindexingDeletionRuleTest { private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); @@ -52,7 +52,7 @@ public class ReindexingFilterRuleTest ); - private final ReindexingFilterRule rule = new ReindexingFilterRule( + private final ReindexingDeletionRule rule = new ReindexingDeletionRule( "test-filter-rule", "Remove robot traffic", PERIOD_30_DAYS, @@ -149,7 +149,7 @@ public void test_constructor_nullId_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule(null, "description", PERIOD_30_DAYS, testFilter, null) + () -> new ReindexingDeletionRule(null, "description", PERIOD_30_DAYS, testFilter, null) ); } @@ -158,7 +158,7 @@ public void test_constructor_nullPeriod_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule("test-id", "description", null, testFilter, null) + () -> new ReindexingDeletionRule("test-id", "description", null, testFilter, null) ); } @@ -168,7 +168,7 @@ public void test_constructor_zeroPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter, null) + () -> new ReindexingDeletionRule("test-id", "description", zeroPeriod, testFilter, null) ); } @@ -178,7 +178,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() Period negativePeriod = Period.days(-30); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter, null) + () -> new ReindexingDeletionRule("test-id", "description", negativePeriod, testFilter, null) ); } @@ -187,7 +187,7 @@ public void test_constructor_nullDeleteWhere_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingFilterRule("test-id", "description", PERIOD_30_DAYS, null, null) + () -> new ReindexingDeletionRule("test-id", "description", PERIOD_30_DAYS, null, null) ); } @@ -198,7 +198,7 @@ public void test_constructor_periodWithMonths_succeeds() { // P6M should work - months are valid even though they're variable length Period period = Period.months(6); - ReindexingFilterRule rule = new ReindexingFilterRule( + ReindexingDeletionRule rule = new ReindexingDeletionRule( "test-id", "6 month rule", period, @@ -214,7 +214,7 @@ public void test_constructor_periodWithYears_succeeds() { // P1Y should work - years are valid even though they're variable length Period period = Period.years(1); - ReindexingFilterRule rule = new ReindexingFilterRule( + ReindexingDeletionRule rule = new ReindexingDeletionRule( "test-id", "1 year rule", period, @@ -230,7 +230,7 @@ public void test_constructor_periodWithMixedMonthsAndDays_succeeds() { // P6M15D should work - mixed months and days Period period = Period.months(6).plusDays(15); - ReindexingFilterRule rule = new ReindexingFilterRule( + ReindexingDeletionRule rule = new ReindexingDeletionRule( "test-id", "6 months 15 days rule", period, @@ -246,7 +246,7 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds() { // P1Y3M10D should work - complex period with years, months, and days Period period = Period.years(1).plusMonths(3).plusDays(10); - ReindexingFilterRule rule = new ReindexingFilterRule( + ReindexingDeletionRule rule = new ReindexingDeletionRule( "test-id", "1 year 3 months 10 days rule", period, @@ -264,7 +264,7 @@ public void test_constructor_zeroMonthsPeriod_throwsIllegalArgumentException() Period zeroPeriod = Period.months(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", zeroPeriod, testFilter, null) + () -> new ReindexingDeletionRule("test-id", "description", zeroPeriod, testFilter, null) ); } @@ -275,7 +275,7 @@ public void test_constructor_negativeMonthsPeriod_throwsIllegalArgumentException Period negativePeriod = Period.months(-6); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingFilterRule("test-id", "description", negativePeriod, testFilter, null) + () -> new ReindexingDeletionRule("test-id", "description", negativePeriod, testFilter, null) ); } @@ -288,7 +288,7 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() // Expected threshold: 2025-06-19T12:00:00Z (6 months before reference) Period sixMonths = Period.months(6); - ReindexingFilterRule monthRule = new ReindexingFilterRule( + ReindexingDeletionRule monthRule = new ReindexingDeletionRule( "test-month-rule", "6 months rule", sixMonths, @@ -327,7 +327,7 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() // Expected threshold: 2024-12-19T12:00:00Z (1 year before reference) Period oneYear = Period.years(1); - ReindexingFilterRule yearRule = new ReindexingFilterRule( + ReindexingDeletionRule yearRule = new ReindexingDeletionRule( "test-year-rule", "1 year rule", oneYear, From 62cc5195932d1a08124b17271a77cdbc9adc440a Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 27 Jan 2026 19:00:39 -0600 Subject: [PATCH 33/90] Make projections rule not be additive --- .../ComposingReindexingRuleProvider.java | 9 ++++--- .../InlineReindexingRuleProvider.java | 5 ++-- .../compaction/ReindexingConfigBuilder.java | 25 ++++--------------- .../compaction/ReindexingRuleProvider.java | 11 +++++--- .../ComposingReindexingRuleProviderTest.java | 6 ++--- .../InlineReindexingRuleProviderTest.java | 3 +-- 6 files changed, 24 insertions(+), 35 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 04547f14e210..536cec4e5718 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -234,13 +234,14 @@ public List getProjectionRules() } @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getProjectionRules(interval, referenceTime)) - .filter(rules -> !rules.isEmpty()) + .map(p -> p.getProjectionRule(interval, referenceTime)) + .filter(Objects::nonNull) .findFirst() - .orElse(Collections.emptyList()); + .orElse(null); } @Override diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index 719b0b53b7ba..f8e4c7ebe644 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -231,9 +231,10 @@ public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime refere } @Override - public List getProjectionRules(Interval interval, DateTime referenceTime) + @Nullable + public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingProjectionRules, interval, referenceTime); + return getApplicableRule(reindexingProjectionRules, interval, referenceTime); } @Override diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index ee991918ff69..923e56ab9e56 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -19,7 +19,6 @@ package org.apache.druid.server.compaction; -import org.apache.druid.data.input.impl.AggregateProjectionSpec; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; @@ -37,7 +36,6 @@ import java.util.List; import java.util.function.Consumer; import java.util.function.Function; -import java.util.stream.Collectors; /** * Builds compaction configs by applying reindexing rules. @@ -101,7 +99,11 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) ReindexingIOConfigRule::getIoConfig ); - count += applyProjectionRules(builder); + count += applyIfPresent( + builder::withProjections, + provider.getProjectionRule(interval, referenceTime), + ReindexingProjectionRule::getProjections + ); count += applyFilterRules(builder); @@ -127,23 +129,6 @@ private int applyIfPresent( return 0; } - private int applyProjectionRules(InlineSchemaDataSourceCompactionConfig.Builder builder) - { - List rules = provider.getProjectionRules(interval, referenceTime); - if (rules.isEmpty()) { - return 0; - } - - // Combine: flatMap all projections from all rules - List combined = rules.stream() - .flatMap(rule -> rule.getProjections().stream()) - .collect(Collectors.toList()); - - builder.withProjections(combined); - LOG.debug("Applied [%d] projection rules for interval %s", rules.size(), interval); - return rules.size(); - } - private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder builder) { List rules = provider.getDeletionRules(interval, referenceTime); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index f1f3e4c98de5..321795d553f8 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -158,16 +158,19 @@ default boolean isReady() List getIOConfigRules(); /** - * Returns all reindexing projection rules that apply to the given interval. + * Returns the matched reindexing projection rule that applies to the given interval. *

- * Handling partial overlaps is the responsibility of the provider implementation and should be clearly documented. + * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. *

+ * * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return The list of {@link ReindexingProjectionRule} rules that apply to the given interval. + * @return {@link ReindexingProjectionRule} rule that applies to the given interval. */ - List getProjectionRules(Interval interval, DateTime referenceTime); + @Nullable + ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime); /** * Returns ALL reindexing projection rules. diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index be52b715b178..d2c488b06c6f 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -265,11 +265,11 @@ public void test_getProjectionRules_compositingBehavior() } @Test - public void test_getProjectionRulesWithInterval_compositingBehavior() + public void test_getProjectionRuleWithInterval_compositingBehavior() { - testComposingBehaviorForAdditiveRuleTypeWithInterval( + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), - (provider, it) -> provider.getProjectionRules(it.interval, it.time), + (provider, it) -> provider.getProjectionRule(it.interval, it.time), createProjectionRule("rule1", Period.days(7)), createProjectionRule("rule2", Period.days(30)), ReindexingProjectionRule::getId diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index a50c83751932..12254b115e30 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -184,8 +184,7 @@ public void test_allRuleTypesWireCorrectly_withInterval() Assert.assertEquals("ioconfig", provider.getIOConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals(1, provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("projection", provider.getProjectionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assert.assertEquals("projection", provider.getProjectionRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); Assert.assertEquals("granularity", provider.getGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); From d70653908158f4a07a5dcd33585ca6e97c58ea23 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 28 Jan 2026 14:10:46 -0600 Subject: [PATCH 34/90] fix a test now that projections rules are not additive --- .../druid/server/compaction/ReindexingConfigBuilderTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 03fd495be188..d3ed393775c4 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -62,7 +62,7 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount int count = configBuilder.applyTo(builder); - Assert.assertEquals(9, count); // 5 non-additive + 2 projection rules + 2 filter rules + Assert.assertEquals(8, count); // 6 non-additive + 2 filter rules InlineSchemaDataSourceCompactionConfig config = builder.build(); @@ -78,7 +78,7 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount Assert.assertNotNull(config.getIoConfig()); Assert.assertNotNull(config.getProjections()); - Assert.assertEquals(3, config.getProjections().size()); // 2 from rule1 + 1 from rule2 + Assert.assertEquals(1, config.getProjections().size()); // 1 from rule2 Assert.assertNotNull(config.getTransformSpec()); DimFilter appliedFilter = config.getTransformSpec().getFilter(); From 5c3b5c5c00e4f1b3e6f473adbef1771248726b11 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 28 Jan 2026 21:09:28 -0600 Subject: [PATCH 35/90] Add two new skip offset concepts to cascading reindexing template + improve testability --- .../compact/CompactionSupervisorTest.java | 4 + .../compact/CascadingReindexingTemplate.java | 125 ++++++- .../CascadingReindexingTemplateTest.java | 329 +++++++++++++++++- 3 files changed, 440 insertions(+), 18 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index ac167e8d2434..85c56819209d 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -324,6 +324,8 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac null, ruleProvider.build(), compactionEngine, + null, + null, null ); runCompactionWithSpec(cascadingReindexingTemplate); @@ -401,6 +403,8 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec .tuningConfigRules(List.of(tuningConfigRule)) .build(), compactionEngine, + null, + null, null ); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index bf3e75adc610..f1d95355024d 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -25,6 +25,7 @@ import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.java.util.common.DateTimes; +import org.apache.druid.java.util.common.IAE; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; @@ -44,9 +45,7 @@ import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.timeline.CompactionState; -import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentTimeline; -import org.apache.druid.timeline.TimelineObjectHolder; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -95,6 +94,8 @@ public class CascadingReindexingTemplate implements CompactionJobTemplate, DataS private final CompactionEngine engine; private final int taskPriority; private final long inputSegmentSizeBytes; + private final Period skipOffsetFromLatest; + private final Period skipOffsetFromNow; @JsonCreator public CascadingReindexingTemplate( @@ -103,7 +104,9 @@ public CascadingReindexingTemplate( @JsonProperty("inputSegmentSizeBytes") @Nullable Long inputSegmentSizeBytes, @JsonProperty("ruleProvider") ReindexingRuleProvider ruleProvider, @JsonProperty("engine") @Nullable CompactionEngine engine, - @JsonProperty("taskContext") @Nullable Map taskContext + @JsonProperty("taskContext") @Nullable Map taskContext, + @JsonProperty("skipOffsetFromLatest") @Nullable Period skipOffsetFromLatest, + @JsonProperty("skipOffsetFromNow") @Nullable Period skipOffsetFromNow ) { this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); @@ -112,6 +115,20 @@ public CascadingReindexingTemplate( this.taskContext = taskContext; this.taskPriority = Objects.requireNonNullElse(taskPriority, DEFAULT_COMPACTION_TASK_PRIORITY); this.inputSegmentSizeBytes = Objects.requireNonNullElse(inputSegmentSizeBytes, DEFAULT_INPUT_SEGMENT_SIZE_BYTES); + + if (skipOffsetFromNow != null && skipOffsetFromLatest != null) { + throw new IAE("Cannot set both skipOffsetFromNow and skipOffsetFromLatest"); + } + if (skipOffsetFromLatest != null) { + this.skipOffsetFromLatest = skipOffsetFromLatest; + this.skipOffsetFromNow = null; + } else if (skipOffsetFromNow != null) { + this.skipOffsetFromNow = skipOffsetFromNow; + this.skipOffsetFromLatest = null; + } else { + this.skipOffsetFromLatest = null; + this.skipOffsetFromNow = null; + } } @Override @@ -163,6 +180,21 @@ private ReindexingRuleProvider getRuleProvider() return ruleProvider; } + @Override + @JsonProperty + @Nullable + public Period getSkipOffsetFromLatest() + { + return skipOffsetFromLatest; + } + + @JsonProperty + @Nullable + private Period getSkipOffsetFromNow() + { + return skipOffsetFromNow; + } + /** * Creates a config finalizer that optimizes filter rules for cascading reindexing. * When a candidate segment has already been reindexed with a subset of filter rules, @@ -256,15 +288,25 @@ public List createCompactionJobs( return Collections.emptyList(); } - // Full data range covered by the timeline - TimelineObjectHolder first = timeline.first(); - TimelineObjectHolder last = timeline.last(); - Interval dataRange = new Interval(first.getInterval().getStart(), last.getInterval().getEnd()); + Interval adjustedTimelineInterval = applySkipOffset( + new Interval(timeline.first().getInterval().getStart(), timeline.last().getInterval().getEnd()), + jobParams.getScheduleStartTime() + ); + if (adjustedTimelineInterval == null) { + LOG.warn("All data for dataSource[%s] is within skip offsets, no reindexing jobs will be created", dataSource); + return Collections.emptyList(); + } for (Interval reindexingInterval : intervals) { - if (!reindexingInterval.overlaps(dataRange)) { - LOG.info("Search interval[%s] does not overlap with data range[%s], skipping", reindexingInterval, dataRange); + if (!reindexingInterval.overlaps(adjustedTimelineInterval)) { + LOG.debug("Search interval[%s] does not overlap with data range[%s], skipping", reindexingInterval, adjustedTimelineInterval); + continue; + } + + reindexingInterval = clampIntervalToBounds(reindexingInterval, adjustedTimelineInterval); + if (reindexingInterval == null) { + LOG.warn("Clamped reindexing interval is empty after applying bounds, meaning no search interval exists. Skipping."); continue; } @@ -277,7 +319,7 @@ public List createCompactionJobs( LOG.info("Creating reindexing jobs for interval[%s] with [%d] rules selected", reindexingInterval, ruleCount); allJobs.addAll( createJobsForSearchInterval( - new CompactionConfigBasedJobTemplate(builder.build(), createCascadingFinalizer()), + createJobTemplateForInterval(builder.build()), reindexingInterval, source, jobParams @@ -290,6 +332,63 @@ public List createCompactionJobs( return allJobs; } + protected CompactionJobTemplate createJobTemplateForInterval( + InlineSchemaDataSourceCompactionConfig config + ) + { + return new CompactionConfigBasedJobTemplate(config, createCascadingFinalizer()); + } + + @Nullable + private Interval clampIntervalToBounds(Interval interval, Interval bounds) + { + DateTime start = interval.getStart(); + DateTime end = interval.getEnd(); + + if (start.isBefore(bounds.getStart())) { + LOG.debug( + "Adjusting start of search interval[%s] to match bounds start[%s]", + interval, + bounds.getStart() + ); + start = bounds.getStart(); + } + + if (end.isAfter(bounds.getEnd())) { + LOG.debug( + "Adjusting end of search interval[%s] to match bounds end[%s]", + interval, + bounds.getEnd() + ); + end = bounds.getEnd(); + } + + if (end.isBefore(start)) { + return null; + } + + return new Interval(start, end); + } + + @Nullable + private Interval applySkipOffset( + Interval interval, + DateTime skipFromNowReferenceTime + ) + { + DateTime maybeAdjustedEnd = interval.getEnd(); + if (skipOffsetFromNow != null) { + maybeAdjustedEnd = skipFromNowReferenceTime.minus(skipOffsetFromNow); + } else if (skipOffsetFromLatest != null) { + maybeAdjustedEnd = maybeAdjustedEnd.minus(skipOffsetFromLatest); + } + if (maybeAdjustedEnd.isBefore(interval.getStart())) { + return null; + } else { + return new Interval(interval.getStart(), maybeAdjustedEnd); + } + } + private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() { return InlineSchemaDataSourceCompactionConfig.builder() @@ -345,12 +444,6 @@ public Integer getMaxRowsPerSegment() return 0; } - @Override - public Period getSkipOffsetFromLatest() - { - return null; - } - @Nullable @Override public UserCompactionTaskQueryTuningConfig getTuningConfig() diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 1b7978824857..8bf70a6e5b6b 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -23,21 +23,33 @@ import com.google.common.collect.ImmutableMap; import org.apache.druid.guice.SupervisorModule; import org.apache.druid.indexer.CompactionEngine; +import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.jackson.DefaultObjectMapper; +import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.granularity.Granularities; +import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingGranularityRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.testing.InitializedNullHandlingTest; +import org.apache.druid.timeline.DataSegment; +import org.apache.druid.timeline.SegmentTimeline; import org.easymock.EasyMock; +import org.jetbrains.annotations.Nullable; +import org.joda.time.DateTime; +import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; public class CascadingReindexingTemplateTest extends InitializedNullHandlingTest { @@ -73,7 +85,9 @@ public void test_serde() throws Exception )) .build(), CompactionEngine.NATIVE, - ImmutableMap.of("context_key", "context_value") + ImmutableMap.of("context_key", "context_value"), + null, + null ); final String json = OBJECT_MAPPER.writeValueAsString(template); @@ -105,7 +119,9 @@ public void test_serde_asDataSourceCompactionConfig() throws Exception )) .build(), CompactionEngine.MSQ, - ImmutableMap.of("key", "value") + ImmutableMap.of("key", "value"), + null, + null ); // Serialize and deserialize as DataSourceCompactionConfig interface @@ -137,6 +153,8 @@ public void test_createCompactionJobs_ruleProviderNotReady() null, notReadyProvider, null, + null, + null, null ); @@ -146,4 +164,311 @@ public void test_createCompactionJobs_ruleProviderNotReady() Assert.assertTrue(jobs.isEmpty()); EasyMock.verify(notReadyProvider); } + + @Test + public void test_constructor_setBothSkipOffsetStrategiesThrowsException() + { + final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.replay(mockProvider); + + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> new CascadingReindexingTemplate( + "testDataSource", + null, + null, + mockProvider, + null, + null, + Period.days(7), // skipOffsetFromLatest + Period.days(3) // skipOffsetFromNow + ) + ); + + Assert.assertEquals("Cannot set both skipOffsetFromNow and skipOffsetFromLatest", exception.getMessage()); + EasyMock.verify(mockProvider); + } + + @Test + public void test_createCompactionJobs_simple() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, null, null + ); + + template.createCompactionJobs(mockSource, mockParams); + List processedIntervals = template.getProcessedIntervals(); + + Assert.assertEquals(2, processedIntervals.size()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(10), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromLatest_skipAllOfTime() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, Period.days(100), null + ); + + List jobs = template.createCompactionJobs(mockSource, mockParams); + + Assert.assertTrue(jobs.isEmpty()); + Assert.assertTrue(template.getProcessedIntervals().isEmpty()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromLatest_trimsIntervalEnd() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, Period.days(5), null + ); + + template.createCompactionJobs(mockSource, mockParams); + List processedIntervals = template.getProcessedIntervals(); + + Assert.assertEquals(2, processedIntervals.size()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(15), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromLatest_eliminatesInterval() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, Period.days(30), null + ); + + template.createCompactionJobs(mockSource, mockParams); + List processedIntervals = template.getProcessedIntervals(); + + Assert.assertEquals(1, processedIntervals.size()); + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(40), processedIntervals.get(0).getEnd()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromNow_skipAllOfTime() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, null, Period.days(100) + ); + + List jobs = template.createCompactionJobs(mockSource, mockParams); + + Assert.assertTrue(jobs.isEmpty()); + Assert.assertTrue(template.getProcessedIntervals().isEmpty()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromNow_trimsIntervalEnd() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, null, Period.days(20) + ); + + template.createCompactionJobs(mockSource, mockParams); + List processedIntervals = template.getProcessedIntervals(); + + Assert.assertEquals(2, processedIntervals.size()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(20), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + @Test + public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() + { + DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); + SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); + ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + CompactionJobParams mockParams = createMockParams(referenceTime, timeline); + DruidInputSource mockSource = createMockSource(); + + TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( + "testDS", null, null, mockProvider, null, null, null, Period.days(40) + ); + + template.createCompactionJobs(mockSource, mockParams); + List processedIntervals = template.getProcessedIntervals(); + + Assert.assertEquals(1, processedIntervals.size()); + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(40), processedIntervals.get(0).getEnd()); + + EasyMock.verify(mockProvider, mockParams, mockSource); + } + + private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate + { + // Capture intervals that were processed for assertions + private final List processedIntervals = new ArrayList<>(); + + public TestCascadingReindexingTemplate( + String dataSource, + Integer taskPriority, + Long inputSegmentSizeBytes, + ReindexingRuleProvider ruleProvider, + CompactionEngine engine, + Map taskContext, + Period skipOffsetFromLatest, + Period skipOffsetFromNow + ) + { + super(dataSource, taskPriority, inputSegmentSizeBytes, ruleProvider, + engine, taskContext, skipOffsetFromLatest, skipOffsetFromNow); + } + + public List getProcessedIntervals() + { + return processedIntervals; + } + + @Override + protected CompactionJobTemplate createJobTemplateForInterval( + InlineSchemaDataSourceCompactionConfig config + ) + { + return new CompactionJobTemplate() { + @Override + public String getType() + { + return "test"; + } + + @Override + public @Nullable Granularity getSegmentGranularity() + { + return null; + } + + @Override + public List createCompactionJobs( + DruidInputSource source, + CompactionJobParams params + ) + { + // Record the interval that was processed + processedIntervals.add(source.getInterval()); + + // Return a single mock job + return List.of(); + } + }; + } + } + + private SegmentTimeline createTestTimeline(DateTime start, DateTime end) + { + DataSegment segment = DataSegment.builder() + .dataSource("testDS") + .interval(new Interval(start, end)) + .version("v1") + .size(1000) + .build(); + return SegmentTimeline.forSegments(Collections.singletonList(segment)); + } + + private ReindexingRuleProvider createMockProvider(DateTime referenceTime, List periods) + { + ReindexingGranularityRule mockGranularityRule = new ReindexingGranularityRule( + "test-rule", + null, + Period.days(1), + new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + ); + + ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.expect(mockProvider.isReady()).andReturn(true); + EasyMock.expect(mockProvider.getCondensedAndSortedPeriods(referenceTime)).andReturn(periods); + EasyMock.expect(mockProvider.getGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(mockGranularityRule).anyTimes(); + EasyMock.expect(mockProvider.getMetricsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getDimensionsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getIOConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getProjectionRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getTuningConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getDeletionRules(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(mockProvider); + return mockProvider; + } + + private CompactionJobParams createMockParams(DateTime referenceTime, SegmentTimeline timeline) + { + CompactionJobParams mockParams = EasyMock.createMock(CompactionJobParams.class); + EasyMock.expect(mockParams.getScheduleStartTime()).andReturn(referenceTime).anyTimes(); + EasyMock.expect(mockParams.getTimeline("testDS")).andReturn(timeline); + EasyMock.replay(mockParams); + return mockParams; + } + + private DruidInputSource createMockSource() + { + final Interval[] capturedInterval = new Interval[1]; + + DruidInputSource mockSource = EasyMock.createMock(DruidInputSource.class); + EasyMock.expect(mockSource.withInterval(EasyMock.anyObject(Interval.class))) + .andAnswer(() -> { + capturedInterval[0] = (Interval) EasyMock.getCurrentArguments()[0]; + return mockSource; + }) + .anyTimes(); + EasyMock.expect(mockSource.getInterval()) + .andAnswer(() -> capturedInterval[0]) + .anyTimes(); + EasyMock.replay(mockSource); + return mockSource; + } } From 0fbbeb2b4c99cfad378a178ef002fd24ed67ff2d Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 29 Jan 2026 16:11:24 -0600 Subject: [PATCH 36/90] Split out segment and query granularity rules --- .../compact/CompactionSupervisorTest.java | 12 +- .../CascadingReindexingTemplateTest.java | 26 +-- .../ComposingReindexingRuleProvider.java | 75 ++++--- .../InlineReindexingRuleProvider.java | 67 ++++-- .../compaction/ReindexingConfigBuilder.java | 50 ++++- .../compaction/ReindexingGranularityRule.java | 76 ------- .../ReindexingQueryGranularityRule.java | 82 +++++++ .../compaction/ReindexingRuleProvider.java | 30 ++- .../ReindexingSegmentGranularityRule.java | 71 +++++++ .../ComposingReindexingRuleProviderTest.java | 67 ++++-- .../InlineReindexingRuleProviderTest.java | 162 +++++++++++--- .../ReindexingConfigBuilderTest.java | 24 ++- .../ReindexingQueryGranularityRuleTest.java | 201 ++++++++++++++++++ ...ReindexingSegmentGranularityRuleTest.java} | 52 ++--- 14 files changed, 764 insertions(+), 231 deletions(-) delete mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java create mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java create mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java rename server/src/test/java/org/apache/druid/server/compaction/{ReindexingGranularityRuleTest.java => ReindexingSegmentGranularityRuleTest.java} (71%) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 85c56819209d..d5014e3aa09a 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -53,8 +53,8 @@ import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingDeletionRule; -import org.apache.druid.server.compaction.ReindexingGranularityRule; import org.apache.druid.server.compaction.ReindexingIOConfigRule; +import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; @@ -279,17 +279,17 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ); Assertions.assertEquals(16, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); - ReindexingGranularityRule hourRule = new ReindexingGranularityRule( + ReindexingSegmentGranularityRule hourRule = new ReindexingSegmentGranularityRule( "hourRule", "Compact to HOUR granularity for data older than 1 days", Period.days(1), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, false) + Granularities.HOUR ); - ReindexingGranularityRule dayRule = new ReindexingGranularityRule( + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule( "dayRule", "Compact to DAY granularity for data older than 7 days", Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + Granularities.DAY ); ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( @@ -308,7 +308,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ); InlineReindexingRuleProvider.Builder ruleProvider = InlineReindexingRuleProvider.builder() - .granularityRules(List.of(hourRule, dayRule)) + .segmentGranularityRules(List.of(hourRule, dayRule)) .tuningConfigRules(List.of(tuningConfigRule)) .deletionRules(List.of(deletionRule)); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 8bf70a6e5b6b..13507e25f8f3 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -29,11 +29,10 @@ import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; -import org.apache.druid.server.compaction.ReindexingGranularityRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.testing.InitializedNullHandlingTest; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentTimeline; @@ -69,18 +68,18 @@ public void test_serde() throws Exception 50, 1000000L, InlineReindexingRuleProvider.builder() - .granularityRules(List.of( - new ReindexingGranularityRule( + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule( "hourRule", null, Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + Granularities.HOUR ), - new ReindexingGranularityRule( + new ReindexingSegmentGranularityRule( "dayRule", null, Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, null) + Granularities.DAY ) )) .build(), @@ -109,12 +108,12 @@ public void test_serde_asDataSourceCompactionConfig() throws Exception 30, 500000L, InlineReindexingRuleProvider.builder() - .granularityRules(List.of( - new ReindexingGranularityRule( + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule( "rule1", null, Period.days(7), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + Granularities.HOUR ) )) .build(), @@ -424,17 +423,18 @@ private SegmentTimeline createTestTimeline(DateTime start, DateTime end) private ReindexingRuleProvider createMockProvider(DateTime referenceTime, List periods) { - ReindexingGranularityRule mockGranularityRule = new ReindexingGranularityRule( + ReindexingSegmentGranularityRule mockGranularityRule = new ReindexingSegmentGranularityRule( "test-rule", null, Period.days(1), - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + Granularities.HOUR ); ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.expect(mockProvider.isReady()).andReturn(true); EasyMock.expect(mockProvider.getCondensedAndSortedPeriods(referenceTime)).andReturn(periods); - EasyMock.expect(mockProvider.getGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(mockGranularityRule).anyTimes(); + EasyMock.expect(mockProvider.getSegmentGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(mockGranularityRule).anyTimes(); + EasyMock.expect(mockProvider.getQueryGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getMetricsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getDimensionsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getIOConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 536cec4e5718..cd41fe7facd9 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -56,36 +56,42 @@ * "providers": [ * { * "type": "inline", - * "granularityRules": [{ - * "id": "recent-data-granularity", - * "olderThan": "P7D", - * "granularity": "HOUR" - * }] + * "segmentGranularityRules": [ + * { + * "id": "recent-data-granularity", + * "olderThan": "P7D", + * "segmentGranularity": "HOUR" + * } + * ] * }, * { * "type": "inline", - * "granularityRules": [{ - * "id": "default-granularity", - * "olderThan": "P1D", - * "granularity": "DAY" - * }], - * "deletionRules": [{ - * "id": "remove-bots", - * "olderThan": "P30D", - * "deleteWhere": { - * "type": "selector", - * "dimension": "isRobot", - * "value": "true" + * "segmentGranularityRules": [ + * { + * "id": "default-granularity", + * "olderThan": "P1D", + * "segmentGranularity": "DAY" + * } + * ], + * "deletionRules": [ + * { + * "id": "remove-bots", + * "olderThan": "P30D", + * "deleteWhere": { + * "type": "selector", + * "dimension": "isRobot", + * "value": "true" + * } * } - * }] + * ] * } * ] * } * }
* In this example: *
    - *
  • Granularity rules come from the first provider (HOUR granularity for recent data)
  • - *
  • Filter rules come from the second provider (first provider with filters)
  • + *
  • Granularity rules come from the first provider (HOUR segment granularity for recent data)
  • + *
  • Deletion rules come from the second provider (first provider with rules)
  • *
*/ public class ComposingReindexingRuleProvider implements ReindexingRuleProvider @@ -245,10 +251,21 @@ public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime re } @Override - public List getGranularityRules() + @Nullable + public ReindexingSegmentGranularityRule getSegmentGranularityRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(ReindexingRuleProvider::getGranularityRules) + .map(p -> p.getSegmentGranularityRule(interval, referenceTime)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @Override + public List getSegmentGranularityRules() + { + return providers.stream() + .map(ReindexingRuleProvider::getSegmentGranularityRules) .filter(rules -> !rules.isEmpty()) .findFirst() .orElse(Collections.emptyList()); @@ -256,15 +273,25 @@ public List getGranularityRules() @Override @Nullable - public ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime) + public ReindexingQueryGranularityRule getQueryGranularityRule(Interval interval, DateTime referenceTime) { return providers.stream() - .map(p -> p.getGranularityRule(interval, referenceTime)) + .map(p -> p.getQueryGranularityRule(interval, referenceTime)) .filter(Objects::nonNull) .findFirst() .orElse(null); } + @Override + public List getQueryGranularityRules() + { + return providers.stream() + .map(ReindexingRuleProvider::getQueryGranularityRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + @Override public List getTuningConfigRules() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index f8e4c7ebe644..be43dee22d8c 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -94,7 +94,8 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider private final List reindexingDimensionsRules; private final List reindexingIOConfigRules; private final List reindexingProjectionRules; - private final List reindexingGranularityRules; + private final List reindexingSegmentGranularityRules; + private final List reindexingQueryGranularityRules; private final List reindexingTuningConfigRules; @@ -105,7 +106,8 @@ public InlineReindexingRuleProvider( @JsonProperty("reindexingDimensionsRules") @Nullable List reindexingDimensionsRules, @JsonProperty("reindexingIOConfigRules") @Nullable List reindexingIOConfigRules, @JsonProperty("reindexingProjectionRules") @Nullable List reindexingProjectionRules, - @JsonProperty("reindexingGranularityRules") @Nullable List reindexingGranularityRules, + @JsonProperty("reindexingSegmentGranularityRules") @Nullable List reindexingSegmentGranularityRules, + @JsonProperty("reindexingQueryGranularityRules") @Nullable List reindexingQueryGranularityRules, @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules ) { @@ -114,7 +116,8 @@ public InlineReindexingRuleProvider( this.reindexingDimensionsRules = Configs.valueOrDefault(reindexingDimensionsRules, Collections.emptyList()); this.reindexingIOConfigRules = Configs.valueOrDefault(reindexingIOConfigRules, Collections.emptyList()); this.reindexingProjectionRules = Configs.valueOrDefault(reindexingProjectionRules, Collections.emptyList()); - this.reindexingGranularityRules = Configs.valueOrDefault(reindexingGranularityRules, Collections.emptyList()); + this.reindexingSegmentGranularityRules = Configs.valueOrDefault(reindexingSegmentGranularityRules, Collections.emptyList()); + this.reindexingQueryGranularityRules = Configs.valueOrDefault(reindexingQueryGranularityRules, Collections.emptyList()); this.reindexingTuningConfigRules = Configs.valueOrDefault(reindexingTuningConfigRules, Collections.emptyList()); } @@ -166,10 +169,17 @@ public List getProjectionRules() } @Override - @JsonProperty("reindexingGranularityRules") - public List getGranularityRules() + @JsonProperty("reindexingQueryGranularityRules") + public List getQueryGranularityRules() { - return reindexingGranularityRules; + return reindexingQueryGranularityRules; + } + + @Override + @JsonProperty("reindexingSegmentGranularityRules") + public List getSegmentGranularityRules() + { + return reindexingSegmentGranularityRules; } @Override @@ -189,7 +199,8 @@ public List getCondensedAndSortedPeriods(DateTime referenceTime) reindexingDimensionsRules, reindexingIOConfigRules, reindexingProjectionRules, - reindexingGranularityRules, + reindexingSegmentGranularityRules, + reindexingQueryGranularityRules, reindexingTuningConfigRules ) .flatMap(List::stream) @@ -239,9 +250,22 @@ public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime re @Override @Nullable - public ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime) + public ReindexingSegmentGranularityRule getSegmentGranularityRule( + Interval interval, + DateTime referenceTime + ) { - return getApplicableRule(reindexingGranularityRules, interval, referenceTime); + return getApplicableRule(reindexingSegmentGranularityRules, interval, referenceTime); + } + + @Override + @Nullable + public ReindexingQueryGranularityRule getQueryGranularityRule( + Interval interval, + DateTime referenceTime + ) + { + return getApplicableRule(reindexingQueryGranularityRules, interval, referenceTime); } @Override @@ -312,7 +336,8 @@ public boolean equals(Object o) && Objects.equals(reindexingDimensionsRules, that.reindexingDimensionsRules) && Objects.equals(reindexingIOConfigRules, that.reindexingIOConfigRules) && Objects.equals(reindexingProjectionRules, that.reindexingProjectionRules) - && Objects.equals(reindexingGranularityRules, that.reindexingGranularityRules) + && Objects.equals(reindexingSegmentGranularityRules, that.reindexingSegmentGranularityRules) + && Objects.equals(reindexingQueryGranularityRules, that.reindexingQueryGranularityRules) && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules); } @@ -325,7 +350,8 @@ public int hashCode() reindexingDimensionsRules, reindexingIOConfigRules, reindexingProjectionRules, - reindexingGranularityRules, + reindexingSegmentGranularityRules, + reindexingQueryGranularityRules, reindexingTuningConfigRules ); } @@ -339,7 +365,8 @@ public String toString() + ", reindexingDimensionsRules=" + reindexingDimensionsRules + ", reindexingIOConfigRules=" + reindexingIOConfigRules + ", reindexingProjectionRules=" + reindexingProjectionRules - + ", reindexingGranularityRules=" + reindexingGranularityRules + + ", reindexingSegmentGranularityRules=" + reindexingSegmentGranularityRules + + ", reindexingQueryGranularityRules=" + reindexingQueryGranularityRules + ", reindexingTuningConfigRules=" + reindexingTuningConfigRules + '}'; } @@ -351,7 +378,8 @@ public static class Builder private List reindexingDimensionsRules; private List reindexingIOConfigRules; private List reindexingProjectionRules; - private List reindexingGranularityRules; + private List reindexingSegmentGranularityRules; + private List reindexingQueryGranularityRules; private List reindexingTuningConfigRules; public Builder deletionRules(List reindexingDeletionRules) @@ -384,9 +412,15 @@ public Builder projectionRules(List reindexingProjecti return this; } - public Builder granularityRules(List reindexingGranularityRules) + public Builder segmentGranularityRules(List reindexingSegmentGranularityRules) + { + this.reindexingSegmentGranularityRules = reindexingSegmentGranularityRules; + return this; + } + + public Builder queryGranularityRules(List reindexingQueryGranularityRules) { - this.reindexingGranularityRules = reindexingGranularityRules; + this.reindexingQueryGranularityRules = reindexingQueryGranularityRules; return this; } @@ -404,7 +438,8 @@ public InlineReindexingRuleProvider build() reindexingDimensionsRules, reindexingIOConfigRules, reindexingProjectionRules, - reindexingGranularityRules, + reindexingSegmentGranularityRules, + reindexingQueryGranularityRules, reindexingTuningConfigRules ); } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index 923e56ab9e56..d0dbccdc09d0 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -19,6 +19,7 @@ package org.apache.druid.server.compaction; +import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; @@ -27,6 +28,7 @@ import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -69,12 +71,6 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) { int count = 0; - count += applyIfPresent( - builder::withGranularitySpec, - provider.getGranularityRule(interval, referenceTime), - ReindexingGranularityRule::getGranularityConfig - ); - count += applyIfPresent( builder::withTuningConfig, provider.getTuningConfigRule(interval, referenceTime), @@ -105,6 +101,8 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) ReindexingProjectionRule::getProjections ); + count += applyGranularityRules(builder); + count += applyFilterRules(builder); return count; @@ -129,6 +127,46 @@ private int applyIfPresent( return 0; } + private int applyGranularityRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + { + ReindexingSegmentGranularityRule segmentGranularityRule = provider.getSegmentGranularityRule( + interval, + referenceTime + ); + ReindexingQueryGranularityRule queryGranularityRule = provider.getQueryGranularityRule( + interval, + referenceTime + ); + + if (segmentGranularityRule == null && queryGranularityRule == null) { + return 0; + } + + // Extract granularities from rules (null if rule doesn't exist) + Granularity segmentGranularity = segmentGranularityRule != null ? segmentGranularityRule.getSegmentGranularity() : null; + + Granularity queryGranularity = queryGranularityRule != null ? queryGranularityRule.getQueryGranularity() : null; + Boolean rollup = queryGranularityRule != null ? queryGranularityRule.getRollup() : null; + + // Build and apply the combined granularity config + UserCompactionTaskGranularityConfig granularityConfig = + new UserCompactionTaskGranularityConfig(segmentGranularity, queryGranularity, rollup); + + builder.withGranularitySpec(granularityConfig); + + int count = 0; + if (segmentGranularityRule != null) { + LOG.debug("Applied segment granularity rule [%s] for interval [%s]", segmentGranularityRule.getId(), interval); + count++; + } + if (queryGranularityRule != null) { + LOG.debug("Applied query granularity rule [%s] for interval [%s]", queryGranularityRule.getId(), interval); + count++; + } + + return count; + } + private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder builder) { List rules = provider.getDeletionRules(interval, referenceTime); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java deleted file mode 100644 index c5ab92a7b8e0..000000000000 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingGranularityRule.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.compaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; -import org.joda.time.Period; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * A {@link ReindexingRule} that specifies a {@link UserCompactionTaskDimensionsConfig} for tasks to configure. - *

- * This rule controls how time-series data is bucketed during reindexing as well as the granularity of individual rows - * written to segments. For example, changing from 15-minute segments to hourly segments reduces segment count and - * improves query performance for older data that doesn't require fine-grained time resolution. - *

- * This is a non-additive rule. Multiple granularity rules cannot be applied to the same interval, - * as a segment can only have one granularity for each of query and segment granularity. - *

- * Example inline usage: - *

{@code
- * {
- *   "id": "daily-30d",
- *   "olderThan": "P30D",
- *   "granularityConfig": {
- *     "segmentGranularity": "DAY",
- *     "queryGranularity": "HOUR"
- *   },
- *   "description": "Compact to daily segments with hour query granularity for data older than 30 days"
- * }
- * }
- */ -public class ReindexingGranularityRule extends AbstractReindexingRule -{ - - private final UserCompactionTaskGranularityConfig granularityConfig; - - @JsonCreator - public ReindexingGranularityRule( - @JsonProperty("id") @Nonnull String id, - @JsonProperty("description") @Nullable String description, - @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("granularityConfig") @Nonnull UserCompactionTaskGranularityConfig granularityConfig) - { - super(id, description, olderThan); - this.granularityConfig = Objects.requireNonNull(granularityConfig, "granularityConfig cannot be null"); - } - - @JsonProperty - public UserCompactionTaskGranularityConfig getGranularityConfig() - { - return granularityConfig; - } -} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java new file mode 100644 index 000000000000..fa176926951e --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java @@ -0,0 +1,82 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.github.resilience4j.core.lang.NonNull; +import org.apache.druid.java.util.common.granularity.Granularity; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A {@link ReindexingRule} that specifies a query granularity for reindexing tasks to configure. + *

+ * This rule controls the granularity of individual rows written to segments. For example, changing from + * minute-level query granularity to hour-level query granularity can reduce data size and improve query performance + * for older data that doesn't require fine-grained time resolution. + *

+ * This is a non-additive rule. Multiple query granularity rules cannot be applied to the same segment. + *

+ * Example inline usage: + *

{@code
+ * {
+ *     "id": "hour-30d",
+ *     "olderThan": "P30D",
+ *     "queryGranularity": "HOUR"
+ *     "rollup": true,
+ *     "description": "Rollup to hour query granularity for data older than 30 days"
+ * }
+ * }
+ */ +public class ReindexingQueryGranularityRule extends AbstractReindexingRule +{ + private final Granularity queryGranularity; + private final Boolean rollup; + + @JsonCreator + public ReindexingQueryGranularityRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("olderThan") @Nonnull Period olderThan, + @JsonProperty("queryGranularity") @NonNull Granularity queryGranularity, + @JsonProperty("rollup") @Nonnull Boolean rollup + ) + { + super(id, description, olderThan); + this.queryGranularity = Objects.requireNonNull(queryGranularity); + this.rollup = Objects.requireNonNull(rollup); + } + + @JsonProperty + public Granularity getQueryGranularity() + { + return queryGranularity; + } + + @JsonProperty + public Boolean getRollup() + { + return rollup; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 321795d553f8..9c6a834acd7c 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -178,7 +178,7 @@ default boolean isReady() List getProjectionRules(); /** - * Returns the matched reindexing granularity rule that applies to the given interval. + * Returns the matched reindexing segment granularity rule that applies to the given interval. *

* Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider * implementation and should be clearly documented. @@ -187,15 +187,35 @@ default boolean isReady() * @param interval The interval to check applicability against. * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return {@link ReindexingGranularityRule} rule that applies to the given interval. + * @return {@link ReindexingSegmentGranularityRule} rule that applies to the given interval. */ @Nullable - ReindexingGranularityRule getGranularityRule(Interval interval, DateTime referenceTime); + ReindexingSegmentGranularityRule getSegmentGranularityRule(Interval interval, DateTime referenceTime); /** - * Returns ALL reindexing granularity rules. + * Returns ALL reindexing segment granularity rules. */ - List getGranularityRules(); + List getSegmentGranularityRules(); + + /** + * Returns the matched reindexing query granularity rule that applies to the given interval. + *

+ * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider + * implementation and should be clearly documented. + *

+ * + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return {@link ReindexingQueryGranularityRule} rule that applies to the given interval. + */ + @Nullable + ReindexingQueryGranularityRule getQueryGranularityRule(Interval interval, DateTime referenceTime); + + /** + * Returns ALL reindexing query granularity rules. + */ + List getQueryGranularityRules(); /** * Returns the matched reindexing tuning config rule that applies to the given interval. diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java new file mode 100644 index 000000000000..b2f8c6480d42 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java @@ -0,0 +1,71 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.java.util.common.granularity.Granularity; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Objects; + +/** + * A {@link ReindexingRule} that specifies a segment granularity for reindexing tasks to configure. + *

+ * This rule controls how time-series data is bucketed into segments during reindexing. For example, changing from + * 15-minute segments to hourly segments reduces segment count. + *

+ * This is a non-additive rule. Multiple segment granularity rules cannot be applied to the same segment. + *

+ * Example inline usage: + *

{@code
+ * {
+ *     "id": "daily-30d",
+ *     "olderThan": "P30D",
+ *     "segmentGranularity": "DAY"
+ *     "description": "Compact to daily segments for data older than 30 days"
+ * }
+ * }
+ */ +public class ReindexingSegmentGranularityRule extends AbstractReindexingRule +{ + private final Granularity segmentGranularity; + + @JsonCreator + public ReindexingSegmentGranularityRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("olderThan") @Nonnull Period olderThan, + @JsonProperty("segmentGranularity") @Nonnull Granularity segmentGranularity + ) + { + super(id, description, olderThan); + this.segmentGranularity = Objects.requireNonNull(segmentGranularity); + } + + @JsonProperty + public Granularity getSegmentGranularity() + { + return segmentGranularity; + } + +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index d2c488b06c6f..4ba9df6714ba 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -28,7 +28,6 @@ import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; @@ -145,14 +144,26 @@ public void test_getDeletionRulesWithInterval_compositingBehavior() } @Test - public void test_getGranularityRules_compositingBehavior() + public void test_getSegmentGranularityRules_compositingBehavior() { testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().granularityRules(rules).build(), - ComposingReindexingRuleProvider::getGranularityRules, - createGranularityRule("rule1", Period.days(7)), - createGranularityRule("rule2", Period.days(30)), - ReindexingGranularityRule::getId + rules -> InlineReindexingRuleProvider.builder().segmentGranularityRules(rules).build(), + ComposingReindexingRuleProvider::getSegmentGranularityRules, + createSegmentGranularityRule("rule1", Period.days(7)), + createSegmentGranularityRule("rule2", Period.days(30)), + ReindexingSegmentGranularityRule::getId + ); + } + + @Test + public void test_getQueryGranularityRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().queryGranularityRules(rules).build(), + ComposingReindexingRuleProvider::getQueryGranularityRules, + createQueryGranularityRule("rule1", Period.days(7)), + createQueryGranularityRule("rule2", Period.days(30)), + ReindexingQueryGranularityRule::getId ); } @@ -301,17 +312,28 @@ public void test_getTuningConfigRuleWithInterval_compositingBehavior() } @Test - public void test_getGranularityRuleWithInterval_compositingBehavior() + public void test_getSegmentGranularityRuleWithInterval_compositingBehavior() { testComposingBehaviorForNonAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().granularityRules(rules).build(), - (provider, it) -> provider.getGranularityRule(it.interval, it.time), - createGranularityRule("rule1", Period.days(7)), - createGranularityRule("rule2", Period.days(30)), - ReindexingGranularityRule::getId + rules -> InlineReindexingRuleProvider.builder().segmentGranularityRules(rules).build(), + (provider, it) -> provider.getSegmentGranularityRule(it.interval, it.time), + createSegmentGranularityRule("rule1", Period.days(7)), + createSegmentGranularityRule("rule2", Period.days(30)), + ReindexingSegmentGranularityRule::getId ); } + @Test + public void test_getQueryGranularityRuleWithInterval_compositingBehavior() + { + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().queryGranularityRules(rules).build(), + (provider, it) -> provider.getQueryGranularityRule(it.interval, it.time), + createQueryGranularityRule("rule1", Period.days(7)), + createQueryGranularityRule("rule2", Period.days(30)), + ReindexingQueryGranularityRule::getId + ); + } @Test public void test_equals_sameProviders_returnsTrue() @@ -499,7 +521,7 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( */ private ReindexingRuleProvider createNotReadyProvider() { - return new InlineReindexingRuleProvider(null, null, null, null, null, null, null) + return new InlineReindexingRuleProvider(null, null, null, null, null, null, null, null) { @Override public boolean isReady() @@ -520,13 +542,24 @@ private ReindexingDeletionRule createFilterRule(String id, Period period) ); } - private ReindexingGranularityRule createGranularityRule(String id, Period period) + private ReindexingSegmentGranularityRule createSegmentGranularityRule(String id, Period period) + { + return new ReindexingSegmentGranularityRule( + id, + "Test granularity rule", + period, + Granularities.DAY + ); + } + + private ReindexingQueryGranularityRule createQueryGranularityRule(String id, Period period) { - return new ReindexingGranularityRule( + return new ReindexingQueryGranularityRule( id, "Test granularity rule", period, - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + Granularities.DAY, + false ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 12254b115e30..71d86222bfc1 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -28,7 +28,6 @@ import org.apache.druid.query.aggregation.CountAggregatorFactory; import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; @@ -38,6 +37,7 @@ import org.junit.Test; import java.util.List; +import java.util.function.BiFunction; public class InlineReindexingRuleProviderTest { @@ -59,7 +59,8 @@ public class InlineReindexingRuleProviderTest public void test_constructor_nullListsDefaultToEmpty() { InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider(null, null, null, null, - null, null, null); + null, null, null, null + ); Assert.assertNotNull(provider.getDeletionRules()); Assert.assertTrue(provider.getDeletionRules().isEmpty()); @@ -71,8 +72,10 @@ public void test_constructor_nullListsDefaultToEmpty() Assert.assertTrue(provider.getIOConfigRules().isEmpty()); Assert.assertNotNull(provider.getProjectionRules()); Assert.assertTrue(provider.getProjectionRules().isEmpty()); - Assert.assertNotNull(provider.getGranularityRules()); - Assert.assertTrue(provider.getGranularityRules().isEmpty()); + Assert.assertNotNull(provider.getSegmentGranularityRules()); + Assert.assertTrue(provider.getSegmentGranularityRules().isEmpty()); + Assert.assertNotNull(provider.getQueryGranularityRules()); + Assert.assertTrue(provider.getQueryGranularityRules().isEmpty()); Assert.assertNotNull(provider.getTuningConfigRules()); Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); } @@ -82,12 +85,12 @@ public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() { ReindexingDeletionRule filter30d = createFilterRule("f1", Period.days(30)); ReindexingDeletionRule filter60d = createFilterRule("f2", Period.days(60)); - ReindexingGranularityRule gran30d = createGranularityRule("g1", Period.days(30)); // Duplicate P30D - ReindexingGranularityRule gran90d = createGranularityRule("g2", Period.days(90)); + ReindexingSegmentGranularityRule gran30d = createSegmentGranularityRule("g1", Period.days(30)); // Duplicate P30D + ReindexingSegmentGranularityRule gran90d = createSegmentGranularityRule("g2", Period.days(90)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .deletionRules(ImmutableList.of(filter30d, filter60d)) - .granularityRules(ImmutableList.of(gran30d, gran90d)) + .segmentGranularityRules(ImmutableList.of(gran30d, gran90d)) .build(); List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); @@ -110,7 +113,7 @@ public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() } @Test - public void test_additiveRules_allScenarios() + public void test_reindexingRules_validateAdditivity() { ReindexingDeletionRule rule30d = createFilterRule("filter-30d", Period.days(30)); ReindexingDeletionRule rule60d = createFilterRule("filter-60d", Period.days(60)); @@ -135,23 +138,63 @@ public void test_additiveRules_allScenarios() } @Test - public void test_nonAdditiveRules_allScenarios() + public void test_allNonAdditiveRules_validateNonAdditivity() { - ReindexingGranularityRule rule30d = createGranularityRule("gran-30d", Period.days(30)); - ReindexingGranularityRule rule60d = createGranularityRule("gran-60d", Period.days(60)); - ReindexingGranularityRule rule90d = createGranularityRule("gran-90d", Period.days(90)); + // Test metrics rules + testNonAdditivity( + "metrics", + this::createMetricsRule, + InlineReindexingRuleProvider.Builder::metricsRules, + InlineReindexingRuleProvider::getMetricsRule + ); - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .granularityRules(ImmutableList.of(rule30d, rule60d, rule90d)) - .build(); + // Test dimensions rules + testNonAdditivity( + "dimensions", + this::createDimensionsRule, + InlineReindexingRuleProvider.Builder::dimensionsRules, + InlineReindexingRuleProvider::getDimensionsRule + ); - Assert.assertNull(provider.getGranularityRule(INTERVAL_20_DAYS_OLD, REFERENCE_TIME)); + // Test IOConfig rules + testNonAdditivity( + "ioConfig", + this::createIOConfigRule, + InlineReindexingRuleProvider.Builder::ioConfigRules, + InlineReindexingRuleProvider::getIOConfigRule + ); + + // Test projection rules + testNonAdditivity( + "projection", + this::createProjectionRule, + InlineReindexingRuleProvider.Builder::projectionRules, + InlineReindexingRuleProvider::getProjectionRule + ); - ReindexingGranularityRule oneMatch = provider.getGranularityRule(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("gran-30d", oneMatch.getId()); + // Test segment granularity rules + testNonAdditivity( + "segmentGranularity", + this::createSegmentGranularityRule, + InlineReindexingRuleProvider.Builder::segmentGranularityRules, + InlineReindexingRuleProvider::getSegmentGranularityRule + ); - ReindexingGranularityRule multiMatch = provider.getGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("Should return rule with oldest threshold (P90D)", "gran-90d", multiMatch.getId()); + // Test query granularity rules + testNonAdditivity( + "queryGranularity", + this::createQueryGranularityRule, + InlineReindexingRuleProvider.Builder::queryGranularityRules, + InlineReindexingRuleProvider::getQueryGranularityRule + ); + + // Test tuning config rules + testNonAdditivity( + "tuningConfig", + this::createTuningConfigRule, + InlineReindexingRuleProvider.Builder::tuningConfigRules, + InlineReindexingRuleProvider::getTuningConfigRule + ); } @Test @@ -162,7 +205,8 @@ public void test_allRuleTypesWireCorrectly_withInterval() ReindexingDimensionsRule dimensionsRule = createDimensionsRule("dimensions", Period.days(30)); ReindexingIOConfigRule ioConfigRule = createIOConfigRule("ioconfig", Period.days(30)); ReindexingProjectionRule projectionRule = createProjectionRule("projection", Period.days(30)); - ReindexingGranularityRule granularityRule = createGranularityRule("granularity", Period.days(30)); + ReindexingSegmentGranularityRule segmentGranularityRule = createSegmentGranularityRule("segmentGranularity", Period.days(30)); + ReindexingQueryGranularityRule queryGranularityRule = createQueryGranularityRule("queryGranularity", Period.days(30)); ReindexingTuningConfigRule tuningConfigRule = createTuningConfigRule("tuning", Period.days(30)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() @@ -171,7 +215,8 @@ public void test_allRuleTypesWireCorrectly_withInterval() .dimensionsRules(ImmutableList.of(dimensionsRule)) .ioConfigRules(ImmutableList.of(ioConfigRule)) .projectionRules(ImmutableList.of(projectionRule)) - .granularityRules(ImmutableList.of(granularityRule)) + .segmentGranularityRules(ImmutableList.of(segmentGranularityRule)) + .queryGranularityRules(ImmutableList.of(queryGranularityRule)) .tuningConfigRules(ImmutableList.of(tuningConfigRule)) .build(); @@ -186,11 +231,65 @@ public void test_allRuleTypesWireCorrectly_withInterval() Assert.assertEquals("projection", provider.getProjectionRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("granularity", provider.getGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + Assert.assertEquals("segmentGranularity", provider.getSegmentGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + + Assert.assertEquals("queryGranularity", provider.getQueryGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); Assert.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); } + /** + * Generic test helper for validating non-additive rule behavior. + *

+ * Tests that when multiple rules match an interval, only the rule with the oldest threshold + * (largest period) is returned. + * + * @param ruleTypeName descriptive name for error messages + * @param ruleFactory function to create a rule instance + * @param builderSetter function to set rules on the builder + * @param ruleGetter function to retrieve the applicable rule from the provider + */ + private void testNonAdditivity( + String ruleTypeName, + BiFunction ruleFactory, + BiFunction, InlineReindexingRuleProvider.Builder> builderSetter, + TriFunction ruleGetter + ) + { + T rule30d = ruleFactory.apply(ruleTypeName + "-30d", Period.days(30)); + T rule60d = ruleFactory.apply(ruleTypeName + "-60d", Period.days(60)); + T rule90d = ruleFactory.apply(ruleTypeName + "-90d", Period.days(90)); + + InlineReindexingRuleProvider.Builder builder = InlineReindexingRuleProvider.builder(); + builderSetter.apply(builder, ImmutableList.of(rule30d, rule60d, rule90d)); + InlineReindexingRuleProvider provider = builder.build(); + + Assert.assertNull( + ruleTypeName + ": No rule should match interval that's too recent", + ruleGetter.apply(provider, INTERVAL_20_DAYS_OLD, REFERENCE_TIME) + ); + + T oneMatch = ruleGetter.apply(provider, INTERVAL_50_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals( + ruleTypeName + ": Only 30d rule should match", + ruleTypeName + "-30d", + oneMatch.getId() + ); + + T multiMatch = ruleGetter.apply(provider, INTERVAL_100_DAYS_OLD, REFERENCE_TIME); + Assert.assertEquals( + ruleTypeName + ": Should return rule with oldest threshold (P90D)", + ruleTypeName + "-90d", + multiMatch.getId() + ); + } + + @FunctionalInterface + private interface TriFunction + { + R apply(T t, U u, V v); + } + @Test public void test_getType_returnsInline() { @@ -235,13 +334,24 @@ private ReindexingProjectionRule createProjectionRule(String id, Period period) return new ReindexingProjectionRule(id, null, period, ImmutableList.of(projectionSpec)); } - private ReindexingGranularityRule createGranularityRule(String id, Period period) + private ReindexingSegmentGranularityRule createSegmentGranularityRule(String id, Period period) + { + return new ReindexingSegmentGranularityRule( + id, + null, + period, + Granularities.DAY + ); + } + + private ReindexingQueryGranularityRule createQueryGranularityRule(String id, Period period) { - return new ReindexingGranularityRule( + return new ReindexingQueryGranularityRule( id, null, period, - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + Granularities.DAY, + true ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index d3ed393775c4..799697fb1c2a 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -32,7 +32,6 @@ import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.DateTime; @@ -62,13 +61,17 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount int count = configBuilder.applyTo(builder); - Assert.assertEquals(8, count); // 6 non-additive + 2 filter rules + Assert.assertEquals(9, count); // 7 non-additive + 2 filter rules InlineSchemaDataSourceCompactionConfig config = builder.build(); - Assert.assertNotNull(config.getGranularitySpec()); + Assert.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); Assert.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); + Assert.assertNotNull(config.getGranularitySpec().getQueryGranularity()); + Assert.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); + Assert.assertTrue(config.getGranularitySpec().isRollup()); + Assert.assertNotNull(config.getTuningConfig()); Assert.assertNotNull(config.getMetricsSpec()); Assert.assertEquals(1, config.getMetricsSpec().length); @@ -122,11 +125,19 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() private ReindexingRuleProvider createFullyPopulatedProvider() { - ReindexingGranularityRule granularityRule = new ReindexingGranularityRule( + ReindexingSegmentGranularityRule segmentGranularityRule = new ReindexingSegmentGranularityRule( "gran-30d", null, Period.days(30), - new UserCompactionTaskGranularityConfig(Granularities.DAY, null, false) + Granularities.DAY + ); + + ReindexingQueryGranularityRule queryGranularityRule = new ReindexingQueryGranularityRule( + "query-gran-30d", + null, + Period.days(30), + Granularities.HOUR, + true ); ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( @@ -200,7 +211,8 @@ private ReindexingRuleProvider createFullyPopulatedProvider() ); return InlineReindexingRuleProvider.builder() - .granularityRules(ImmutableList.of(granularityRule)) + .segmentGranularityRules(ImmutableList.of(segmentGranularityRule)) + .queryGranularityRules(ImmutableList.of(queryGranularityRule)) .tuningConfigRules(ImmutableList.of(tuningConfigRule)) .metricsRules(ImmutableList.of(metricsRule)) .dimensionsRules(ImmutableList.of(dimensionsRule)) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java new file mode 100644 index 000000000000..815bd1afe70c --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java @@ -0,0 +1,201 @@ +/* + * 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.compaction; + +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.granularity.Granularity; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.Assert; +import org.junit.Test; + +public class ReindexingQueryGranularityRuleTest +{ + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); + private static final Period PERIOD_7_DAYS = Period.days(7); + + private final ReindexingQueryGranularityRule rule = new ReindexingQueryGranularityRule( + "test-rule", + "Test query granularity rule", + PERIOD_7_DAYS, + Granularities.HOUR, + true + ); + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval ends at 2025-12-10, which is fully before threshold + Interval interval = Intervals.of("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = Intervals.of("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = Intervals.of("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) + // Interval starts after threshold - NONE + Interval interval = Intervals.of("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getQueryGranularity_returnsConfiguredValue() + { + Granularity granularity = rule.getQueryGranularity(); + + Assert.assertNotNull(granularity); + Assert.assertEquals(Granularities.HOUR, granularity); + } + + @Test + public void test_getRollup_returnsConfiguredValue() + { + Boolean rollup = rule.getRollup(); + + Assert.assertNotNull(rollup); + Assert.assertEquals(true, rollup); + } + + @Test + public void test_getRollup_returnsFalse_whenConfiguredFalse() + { + ReindexingQueryGranularityRule ruleWithoutRollup = new ReindexingQueryGranularityRule( + "test-rule-no-rollup", + "Test query granularity rule without rollup", + PERIOD_7_DAYS, + Granularities.HOUR, + false + ); + + Boolean rollup = ruleWithoutRollup.getRollup(); + + Assert.assertNotNull(rollup); + Assert.assertEquals(false, rollup); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assert.assertEquals("test-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assert.assertEquals("Test query granularity rule", rule.getDescription()); + } + + @Test + public void test_getOlderThan_returnsConfiguredPeriod() + { + Assert.assertEquals(PERIOD_7_DAYS, rule.getOlderThan()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new ReindexingQueryGranularityRule(null, "description", PERIOD_7_DAYS, Granularities.HOUR, true) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new ReindexingQueryGranularityRule("test-id", "description", null, Granularities.HOUR, true) + ); + } + + @Test + public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + { + Period zeroPeriod = Period.days(0); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new ReindexingQueryGranularityRule("test-id", "description", zeroPeriod, Granularities.HOUR, true) + ); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-7); + Assert.assertThrows( + IllegalArgumentException.class, + () -> new ReindexingQueryGranularityRule("test-id", "description", negativePeriod, Granularities.HOUR, true) + ); + } + + @Test + public void test_constructor_nullQueryGranularity_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new ReindexingQueryGranularityRule("test-id", "description", PERIOD_7_DAYS, null, true) + ); + } + + @Test + public void test_constructor_nullRollup_throwsNullPointerException() + { + Assert.assertThrows( + NullPointerException.class, + () -> new ReindexingQueryGranularityRule("test-id", "description", PERIOD_7_DAYS, Granularities.HOUR, null) + ); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java similarity index 71% rename from server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java rename to server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java index 4a54943c3164..041e90f45ac2 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java @@ -22,23 +22,23 @@ 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.server.coordinator.UserCompactionTaskGranularityConfig; +import org.apache.druid.java.util.common.granularity.Granularity; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; import org.junit.Test; -public class ReindexingGranularityRuleTest +public class ReindexingSegmentGranularityRuleTest { private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_7_DAYS = Period.days(7); - private final ReindexingGranularityRule rule = new ReindexingGranularityRule( + private final ReindexingSegmentGranularityRule rule = new ReindexingSegmentGranularityRule( "test-rule", - "Test granularity rule", + "Test segment granularity rule", PERIOD_7_DAYS, - new UserCompactionTaskGranularityConfig(Granularities.HOUR, null, null) + Granularities.HOUR ); @Test @@ -90,12 +90,12 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() } @Test - public void test_getGranularityConfig_returnsConfiguredValue() + public void test_getGranularity_returnsConfiguredValue() { - UserCompactionTaskGranularityConfig config = rule.getGranularityConfig(); + Granularity granularity = rule.getSegmentGranularity(); - Assert.assertNotNull(config); - Assert.assertEquals(Granularities.HOUR, config.getSegmentGranularity()); + Assert.assertNotNull(granularity); + Assert.assertEquals(Granularities.HOUR, granularity); } @Test @@ -107,7 +107,7 @@ public void test_getId_returnsConfiguredId() @Test public void test_getDescription_returnsConfiguredDescription() { - Assert.assertEquals("Test granularity rule", rule.getDescription()); + Assert.assertEquals("Test segment granularity rule", rule.getDescription()); } @Test @@ -119,67 +119,47 @@ public void test_getOlderThan_returnsConfiguredPeriod() @Test public void test_constructor_nullId_throwsNullPointerException() { - UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( - Granularities.HOUR, - null, - null - ); Assert.assertThrows( NullPointerException.class, - () -> new ReindexingGranularityRule(null, "description", PERIOD_7_DAYS, config) + () -> new ReindexingSegmentGranularityRule(null, "description", PERIOD_7_DAYS, Granularities.HOUR) ); } @Test public void test_constructor_nullPeriod_throwsNullPointerException() { - UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( - Granularities.HOUR, - null, - null - ); Assert.assertThrows( NullPointerException.class, - () -> new ReindexingGranularityRule("test-id", "description", null, config) + () -> new ReindexingSegmentGranularityRule("test-id", "description", null, Granularities.HOUR) ); } @Test public void test_constructor_zeroPeriod_throwsIllegalArgumentException() { - UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( - Granularities.HOUR, - null, - null - ); Period zeroPeriod = Period.days(0); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingGranularityRule("test-id", "description", zeroPeriod, config) + () -> new ReindexingSegmentGranularityRule("test-id", "description", zeroPeriod, Granularities.HOUR) ); } @Test public void test_constructor_negativePeriod_throwsIllegalArgumentException() { - UserCompactionTaskGranularityConfig config = new UserCompactionTaskGranularityConfig( - Granularities.HOUR, - null, - null - ); Period negativePeriod = Period.days(-7); Assert.assertThrows( IllegalArgumentException.class, - () -> new ReindexingGranularityRule("test-id", "description", negativePeriod, config) + () -> new ReindexingSegmentGranularityRule("test-id", "description", negativePeriod, Granularities.HOUR) ); } @Test - public void test_constructor_nullGranularityConfig_throwsNullPointerException() + public void test_constructor_nullGranularity_throwsNullPointerException() { Assert.assertThrows( NullPointerException.class, - () -> new ReindexingGranularityRule("test-id", "description", PERIOD_7_DAYS, null) + () -> new ReindexingSegmentGranularityRule("test-id", "description", PERIOD_7_DAYS, null) ); } } From 0ea46b76fbac8e745bfc307ecd319d63a66c369e Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 29 Jan 2026 18:40:47 -0600 Subject: [PATCH 37/90] Refactor the non overlapping timeline build for cascading reindexing --- .../compact/CascadingReindexingTemplate.java | 187 +++++++++++++++++- .../CascadingReindexingTemplateTest.java | 149 ++++++++++++++ 2 files changed, 332 insertions(+), 4 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index f1d95355024d..6bfacc2a004f 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -26,6 +26,7 @@ import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; @@ -38,6 +39,7 @@ import org.apache.druid.server.compaction.ReindexingConfigBuilder; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -47,15 +49,18 @@ import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.SegmentTimeline; import org.joda.time.DateTime; +import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.Period; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.stream.Collectors; /** * Template to perform period-based cascading reindexing. {@link ReindexingRule} are provided by a {@link ReindexingRuleProvider} @@ -275,12 +280,10 @@ public List createCompactionJobs( final List allJobs = new ArrayList<>(); final DateTime currentTime = jobParams.getScheduleStartTime(); - List sortedPeriods = ruleProvider.getCondensedAndSortedPeriods(currentTime); - if (sortedPeriods.isEmpty()) { + List intervals = generateAlignedSearchIntervals(currentTime); + if (intervals.isEmpty()) { return Collections.emptyList(); } - - List intervals = generateSearchIntervals(sortedPeriods, currentTime); SegmentTimeline timeline = jobParams.getTimeline(dataSource); if (timeline == null || timeline.isEmpty()) { @@ -400,6 +403,182 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() .withSkipOffsetFromLatest(Period.ZERO); } + /** + * Generates granularity-aligned search intervals based on segment granularity rules, + * then splits them at non-segment-granularity rule thresholds where safe to do so. + *

+ * Algorithm: + * 1. Generate base timeline from segment granularity rules + * 2. Collect thresholds from all non-segment-granularity rules + * 3. For each base interval, find thresholds that fall within it + * 4. Align those thresholds to the interval's segment granularity + * 5. Split intervals at aligned thresholds + * + * @param referenceTime the reference time for calculating period thresholds + * @return list of split and aligned intervals, ordered from oldest to newest + * @throws IAE if no segment granularity rules are found + */ + List generateAlignedSearchIntervals(DateTime referenceTime) + { + // Step 1: Generate base timeline from segment granularity rules + List baseTimeline = generateBaseTimelineWithGranularities(referenceTime); + + // Step 2: Collect thresholds from non-segment-granularity rules + List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); + + // Step 3: For each base interval, collect and apply split points + List finalIntervals = new ArrayList<>(); + + for (IntervalWithGranularity baseInterval : baseTimeline) { + List splitPoints = new ArrayList<>(); + + // Find thresholds that fall within this base interval + for (DateTime threshold : nonSegmentGranThresholds) { + if (threshold.isAfter(baseInterval.interval.getStart()) && + threshold.isBefore(baseInterval.interval.getEnd())) { + + // Align threshold to this interval's segment granularity + DateTime alignedThreshold = baseInterval.granularity.bucketStart(threshold); + + // Only add if it's not at the boundaries (would create zero-length interval) + if (alignedThreshold.isAfter(baseInterval.interval.getStart()) && + alignedThreshold.isBefore(baseInterval.interval.getEnd())) { + splitPoints.add(alignedThreshold); + } + } + } + + // Sort and deduplicate split points + splitPoints = splitPoints.stream() + .distinct() + .sorted() + .collect(Collectors.toList()); + + // Split this base interval at the split points + if (splitPoints.isEmpty()) { + LOG.debug("No splits for interval [%s]", baseInterval.interval); + finalIntervals.add(baseInterval.interval); + } else { + LOG.debug("Splitting interval [%s] at [%d] points", baseInterval.interval, splitPoints.size()); + DateTime start = baseInterval.interval.getStart(); + for (DateTime splitPoint : splitPoints) { + finalIntervals.add(new Interval(start, splitPoint)); + start = splitPoint; + } + // Add the final segment + finalIntervals.add(new Interval(start, baseInterval.interval.getEnd())); + } + } + + return finalIntervals; + } + + /** + * Generates the base timeline with segment granularities tracked. + */ + private List generateBaseTimelineWithGranularities(DateTime referenceTime) + { + List segmentGranRules = ruleProvider.getSegmentGranularityRules(); + + if (segmentGranRules.isEmpty()) { + throw new IAE( + "CascadingReindexingTemplate requires at least one segment granularity rule. " + + "TODO: Support templates with only non-segment-granularity rules" + ); + } + + // Sort rules by period from longest to shortest (oldest to most recent threshold) + List sortedRules = segmentGranRules.stream() + .sorted(Comparator.comparingLong(rule -> { + DateTime threshold = referenceTime.minus(rule.getOlderThan()); + return threshold.getMillis(); // Ascending = oldest first + })) + .collect(Collectors.toList()); + + // Build base timeline with granularities tracked + List baseTimeline = new ArrayList<>(); + DateTime previousAlignedEnd = null; + + for (ReindexingSegmentGranularityRule rule : sortedRules) { + DateTime rawEnd = referenceTime.minus(rule.getOlderThan()); + DateTime alignedEnd = rule.getSegmentGranularity().bucketStart(rawEnd); + DateTime alignedStart = (previousAlignedEnd != null) ? previousAlignedEnd : DateTimes.MIN; + + LOG.debug( + "Base interval for rule [%s]: raw end [%s] -> aligned [%s/%s) with granularity [%s]", + rule.getId(), + rawEnd, + alignedStart, + alignedEnd, + rule.getSegmentGranularity() + ); + + baseTimeline.add(new IntervalWithGranularity( + new Interval(alignedStart, alignedEnd), + rule.getSegmentGranularity() + )); + + previousAlignedEnd = alignedEnd; + } + + return baseTimeline; + } + + /** + * Collects thresholds from all non-segment-granularity rules. + */ + private List collectNonSegmentGranularityThresholds(DateTime referenceTime) + { + List thresholds = new ArrayList<>(); + + // Collect from all rule types + ruleProvider.getMetricsRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getDimensionsRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getIOConfigRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getProjectionRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getQueryGranularityRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getTuningConfigRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + ruleProvider.getDeletionRules().stream() + .map(rule -> referenceTime.minus(rule.getOlderThan())) + .forEach(thresholds::add); + + return thresholds; + } + + /** + * Helper class to track an interval with its associated segment granularity. + */ + private static class IntervalWithGranularity + { + final Interval interval; + final Granularity granularity; + + IntervalWithGranularity(Interval interval, Granularity granularity) + { + this.interval = interval; + this.granularity = granularity; + } + } + + @Deprecated private List generateSearchIntervals(List sortedPeriods, DateTime referenceTime) { List intervals = new ArrayList<>(sortedPeriods.size()); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 13507e25f8f3..f408be88d9ca 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -28,7 +28,9 @@ import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingMetricsRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; @@ -351,6 +353,153 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() EasyMock.verify(mockProvider, mockParams, mockSource); } + @Test + public void test_generateAlignedSearchIntervals_withGranularityAlignment() + { + // Reference time: 2025-01-29T16:15 + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + // Create rules with different periods and granularities + // P7D with HOUR granularity -> raw end: 2025-01-22T16:15 -> aligned: 2025-01-22T16:00 + // P1M with DAY granularity -> raw end: 2024-12-29T16:15 -> aligned: 2024-12-29T00:00 + // P3M with MONTH granularity -> raw end: 2024-10-29T16:15 -> aligned: 2024-10-01T00:00 + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY), + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + // Expected 3 intervals, ordered from oldest to newest: + // 1. [-inf, 2024-10-01T00:00) - P3M rule with MONTH granularity + // 2. [2024-10-01T00:00, 2024-12-29T00:00) - P1M rule with DAY granularity + // 3. [2024-12-29T00:00, 2025-01-22T16:00) - P7D rule with HOUR granularity + + Assert.assertEquals(3, intervals.size()); + + // Interval 1: oldest + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + + // Interval 2: middle + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); + + // Interval 3: most recent + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(2).getEnd()); + } + + @Test + public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSplits() + { + // Reference time: 2025-01-29T16:15 + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + // Segment granularity rules create base timeline: + // [-inf, 2024-10-01T00:00) MONTH + // [2024-10-01T00:00, 2024-12-29T00:00) DAY + // [2024-12-29T00:00, 2025-01-22T16:00) HOUR + // + // Non-segment-gran rules with thresholds: + // P8D -> 2025-01-21T16:15 -> falls in HOUR interval -> aligned: 2025-01-21T16:00 + // P14D -> 2025-01-15T16:15 -> falls in HOUR interval -> aligned: 2025-01-15T16:00 + // P45D -> 2024-12-15T16:15 -> falls in DAY interval -> aligned: 2024-12-15T00:00 + // P100D -> 2024-10-21T16:15 -> falls in MONTH interval -> aligned: 2024-10-01T00:00 (at boundary, no split) + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY), + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH) + )) + .metricsRules(List.of( + new ReindexingMetricsRule( + "metrics-8d", null, Period.days(8), + new AggregatorFactory[0] + ), + new ReindexingMetricsRule( + "metrics-14d", null, Period.days(14), + new AggregatorFactory[0] + ), + new ReindexingMetricsRule( + "metrics-45d", null, Period.days(45), + new AggregatorFactory[0] + ), + new ReindexingMetricsRule( + "metrics-100d", null, Period.days(100), + new AggregatorFactory[0] + ) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + // Expected 7 intervals after splitting: + // 1. [-inf, 2024-10-01T00:00) - no split in MONTH interval + // 2. [2024-10-01T00:00, 2024-10-21T00:00) - split by P100D (falls in DAY interval!) + // 3. [2024-10-21T00:00, 2024-12-15T00:00) - between P100D and P45D + // 4. [2024-12-15T00:00, 2024-12-29T00:00) - split by P45D + // 5. [2024-12-29T00:00, 2025-01-15T16:00) - split by P14D + // 6. [2025-01-15T16:00, 2025-01-21T16:00) - between P14D and P8D + // 7. [2025-01-21T16:00, 2025-01-22T16:00) - split by P8D + + Assert.assertEquals(7, intervals.size()); + + // Interval 1: no split in MONTH interval + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + + // Interval 2: split by P100D (2024-10-21 falls in DAY granularity interval) + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(1).getEnd()); + + // Interval 3: between P100D and P45D + Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(2).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(2).getEnd()); + + // Interval 4: split by P45D + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(3).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(3).getEnd()); + + // Interval 5: split by P14D + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(4).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getEnd()); + + // Interval 6: between P14D and P8D + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(5).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(5).getEnd()); + + // Interval 7: split by P8D + Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(6).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getEnd()); + } + private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate { // Capture intervals that were processed for assertions From 4ad3a9034878eb3b61926e21e79a1b4a3690b45d Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 29 Jan 2026 19:18:57 -0600 Subject: [PATCH 38/90] Add a forced segment gran field to supervisor and some tests --- .../compact/CompactionSupervisorTest.java | 6 +- .../compact/CascadingReindexingTemplate.java | 74 ++++++++- .../CascadingReindexingTemplateTest.java | 155 +++++++++++++++++- 3 files changed, 220 insertions(+), 15 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index d5014e3aa09a..e1b2bf3b6add 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -326,7 +326,8 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac compactionEngine, null, null, - null + null, + Granularities.DAY ); runCompactionWithSpec(cascadingReindexingTemplate); waitForAllCompactionTasksToFinish(); @@ -405,7 +406,8 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec compactionEngine, null, null, - null + null, + Granularities.DAY ); runCompactionWithSpec(cascadingTemplate); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 6bfacc2a004f..80b17167a117 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -26,7 +26,6 @@ import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.IAE; -import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; @@ -49,7 +48,6 @@ import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.SegmentTimeline; import org.joda.time.DateTime; -import org.joda.time.Duration; import org.joda.time.Interval; import org.joda.time.Period; @@ -101,6 +99,7 @@ public class CascadingReindexingTemplate implements CompactionJobTemplate, DataS private final long inputSegmentSizeBytes; private final Period skipOffsetFromLatest; private final Period skipOffsetFromNow; + private final Granularity defaultSegmentGranularity; @JsonCreator public CascadingReindexingTemplate( @@ -111,7 +110,8 @@ public CascadingReindexingTemplate( @JsonProperty("engine") @Nullable CompactionEngine engine, @JsonProperty("taskContext") @Nullable Map taskContext, @JsonProperty("skipOffsetFromLatest") @Nullable Period skipOffsetFromLatest, - @JsonProperty("skipOffsetFromNow") @Nullable Period skipOffsetFromNow + @JsonProperty("skipOffsetFromNow") @Nullable Period skipOffsetFromNow, + @JsonProperty("defaultSegmentGranularity") Granularity defaultSegmentGranularity ) { this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); @@ -120,6 +120,10 @@ public CascadingReindexingTemplate( this.taskContext = taskContext; this.taskPriority = Objects.requireNonNullElse(taskPriority, DEFAULT_COMPACTION_TASK_PRIORITY); this.inputSegmentSizeBytes = Objects.requireNonNullElse(inputSegmentSizeBytes, DEFAULT_INPUT_SEGMENT_SIZE_BYTES); + this.defaultSegmentGranularity = Objects.requireNonNull( + defaultSegmentGranularity, + "'defaultSegmentGranularity' cannot be null" + ); if (skipOffsetFromNow != null && skipOffsetFromLatest != null) { throw new IAE("Cannot set both skipOffsetFromNow and skipOffsetFromLatest"); @@ -200,6 +204,12 @@ private Period getSkipOffsetFromNow() return skipOffsetFromNow; } + @JsonProperty + public Granularity getDefaultSegmentGranularity() + { + return defaultSegmentGranularity; + } + /** * Creates a config finalizer that optimizes filter rules for cascading reindexing. * When a candidate segment has already been reindexed with a subset of filter rules, @@ -475,16 +485,41 @@ List generateAlignedSearchIntervals(DateTime referenceTime) /** * Generates the base timeline with segment granularities tracked. + *

+ * If segment granularity rules exist, uses them to build the base timeline. + * If not, uses the smallest period from non-segment-granularity rules with the default granularity. */ private List generateBaseTimelineWithGranularities(DateTime referenceTime) { List segmentGranRules = ruleProvider.getSegmentGranularityRules(); if (segmentGranRules.isEmpty()) { - throw new IAE( - "CascadingReindexingTemplate requires at least one segment granularity rule. " - + "TODO: Support templates with only non-segment-granularity rules" + // No segment granularity rules - use default granularity with smallest period from other rules + List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); + + if (nonSegmentGranThresholds.isEmpty()) { + throw new IAE( + "CascadingReindexingTemplate requires at least one reindexing rule " + + "(segment granularity or other type)" + ); + } + + // Find the smallest period (most recent threshold = largest DateTime value) + DateTime mostRecentThreshold = Collections.max(nonSegmentGranThresholds); + DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentThreshold); + + LOG.info( + "No segment granularity rules found. Creating base interval with default granularity [%s] " + + "and threshold [%s] (aligned: [%s])", + defaultSegmentGranularity, + mostRecentThreshold, + alignedEnd ); + + return Collections.singletonList(new IntervalWithGranularity( + new Interval(DateTimes.MIN, alignedEnd), + defaultSegmentGranularity + )); } // Sort rules by period from longest to shortest (oldest to most recent threshold) @@ -521,6 +556,33 @@ private List generateBaseTimelineWithGranularities(Date previousAlignedEnd = alignedEnd; } + // Check if we need to prepend an interval for non-segment-gran rules that are more recent + // than the most recent segment gran rule + List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); + if (!nonSegmentGranThresholds.isEmpty()) { + DateTime mostRecentNonSegmentGranThreshold = Collections.max(nonSegmentGranThresholds); + DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).interval.getEnd(); + + if (mostRecentNonSegmentGranThreshold.isAfter(mostRecentSegmentGranEnd)) { + // Need to prepend an interval for more recent non-segment-gran rules + DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); + + LOG.info( + "Most recent non-segment-gran threshold [%s] is after most recent segment gran interval end [%s]. " + + "Prepending interval with default granularity [%s] (aligned end: [%s])", + mostRecentNonSegmentGranThreshold, + mostRecentSegmentGranEnd, + defaultSegmentGranularity, + alignedEnd + ); + + baseTimeline.add(new IntervalWithGranularity( + new Interval(mostRecentSegmentGranEnd, alignedEnd), + defaultSegmentGranularity + )); + } + } + return baseTimeline; } diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index f408be88d9ca..d82e1c09f3ec 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -88,7 +88,8 @@ public void test_serde() throws Exception CompactionEngine.NATIVE, ImmutableMap.of("context_key", "context_value"), null, - null + null, + Granularities.DAY ); final String json = OBJECT_MAPPER.writeValueAsString(template); @@ -122,7 +123,8 @@ public void test_serde_asDataSourceCompactionConfig() throws Exception CompactionEngine.MSQ, ImmutableMap.of("key", "value"), null, - null + null, + Granularities.HOUR ); // Serialize and deserialize as DataSourceCompactionConfig interface @@ -156,7 +158,8 @@ public void test_createCompactionJobs_ruleProviderNotReady() null, null, null, - null + null, + Granularities.DAY ); // Call createCompactionJobs - should return empty list without processing @@ -182,7 +185,8 @@ public void test_constructor_setBothSkipOffsetStrategiesThrowsException() null, null, Period.days(7), // skipOffsetFromLatest - Period.days(3) // skipOffsetFromNow + Period.days(3), // skipOffsetFromNow + Granularities.DAY ) ); @@ -379,7 +383,8 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() null, null, null, - null + null, + Granularities.DAY ); List intervals = template.generateAlignedSearchIntervals(referenceTime); @@ -455,7 +460,8 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl null, null, null, - null + null, + Granularities.DAY ); List intervals = template.generateAlignedSearchIntervals(referenceTime); @@ -500,6 +506,141 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getEnd()); } + @Test + public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() + { + // Reference time: 2025-01-29T16:15 + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + // No segment granularity rules - only metrics rules + // P8D -> 2025-01-21T16:15 (smallest/most recent) + // P14D -> 2025-01-15T16:15 + // P45D -> 2024-12-15T16:15 + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-8d", null, Period.days(8), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-45d", null, Period.days(45), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY // default granularity + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + // Expected: One base interval [-inf, aligned(smallest period)) then split by other rules + // Smallest period is P8D -> threshold: 2025-01-21T16:15 + // Aligned to DAY: 2025-01-21T00:00 + // Then split by P14D (2025-01-15T00:00) and P45D (2024-12-15T00:00) + // + // Result: 4 intervals + // 1. [-inf, 2024-12-15T00:00) + // 2. [2024-12-15T00:00, 2025-01-15T00:00) + // 3. [2025-01-15T00:00, 2025-01-21T00:00) + // 4. [2025-01-21T00:00, 2025-01-21T00:00) - WAIT, this would be zero-length! + + // Actually, P14D and P45D both fall within the base interval [-inf, 2025-01-21T00:00) + // So they will split it: + // 1. [-inf, 2024-12-15T00:00) - before P45D + // 2. [2024-12-15T00:00, 2025-01-15T00:00) - between P45D and P14D + // 3. [2025-01-15T00:00, 2025-01-21T00:00) - after P14D, before base end + + Assert.assertEquals(3, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(0).getEnd()); + + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(1).getEnd()); + + Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(2).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-21T00:00:00Z"), intervals.get(2).getEnd()); + } + + @Test + public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmentGranRules() + { + // Reference time: 2025-01-29T16:15 + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + // Segment granularity rules: + // P1M -> 2024-12-29T16:15 (most recent segment gran threshold) + // P3M -> 2024-10-29T16:15 + // + // Non-segment-gran rules (more recent than P1M!): + // P7D -> 2025-01-22T16:15 + // P14D -> 2025-01-15T16:15 + // P21D -> 2025-01-08T16:15 + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH), + new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-7d", null, Period.days(7), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-21d", null, Period.days(21), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.HOUR // default for prepended interval + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + // Expected base timeline: + // 1. [-inf, 2024-10-01T00:00) - P3M with MONTH gran + // 2. [2024-10-01T00:00, 2024-12-29T00:00) - P1M with DAY gran + // 3. [2024-12-29T00:00, 2025-01-22T16:00) - PREPENDED with HOUR gran (for P7D) + // + // Then split by P14D (2025-01-15T16:00) and P21D (2025-01-08T16:00) which fall in interval 3: + // 3a. [2024-12-29T00:00, 2025-01-08T16:00) + // 3b. [2025-01-08T16:00, 2025-01-15T16:00) + // 3c. [2025-01-15T16:00, 2025-01-22T16:00) + + Assert.assertEquals(5, intervals.size()); + + // Interval 1: oldest + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + + // Interval 2: middle + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); + + // Interval 3a: prepended interval, split by P21D + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(2).getEnd()); + + // Interval 3b: between P21D and P14D + Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(3).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(3).getEnd()); + + // Interval 3c: after P14D + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(4).getEnd()); + } + private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate { // Capture intervals that were processed for assertions @@ -517,7 +658,7 @@ public TestCascadingReindexingTemplate( ) { super(dataSource, taskPriority, inputSegmentSizeBytes, ruleProvider, - engine, taskContext, skipOffsetFromLatest, skipOffsetFromNow); + engine, taskContext, skipOffsetFromLatest, skipOffsetFromNow, Granularities.DAY); } public List getProcessedIntervals() From a97fd185cc62367972fff8907d5f44d47b85029a Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 30 Jan 2026 08:23:14 -0600 Subject: [PATCH 39/90] Remove unused method --- .../compact/CascadingReindexingTemplate.java | 203 +++++++++--------- .../CascadingReindexingTemplateTest.java | 66 +++--- .../ComposingReindexingRuleProvider.java | 20 -- .../InlineReindexingRuleProvider.java | 30 --- .../compaction/ReindexingRuleProvider.java | 35 ++- .../ComposingReindexingRuleProviderTest.java | 24 --- .../InlineReindexingRuleProviderTest.java | 32 --- 7 files changed, 168 insertions(+), 242 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 80b17167a117..238ba5872076 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; import org.apache.druid.data.input.impl.AggregateProjectionSpec; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; @@ -290,10 +291,6 @@ public List createCompactionJobs( final List allJobs = new ArrayList<>(); final DateTime currentTime = jobParams.getScheduleStartTime(); - List intervals = generateAlignedSearchIntervals(currentTime); - if (intervals.isEmpty()) { - return Collections.emptyList(); - } SegmentTimeline timeline = jobParams.getTimeline(dataSource); if (timeline == null || timeline.isEmpty()) { @@ -301,6 +298,12 @@ public List createCompactionJobs( return Collections.emptyList(); } + List searchIntervals = generateAlignedSearchIntervals(currentTime); + if (searchIntervals.isEmpty()) { + LOG.warn("No search intervals generated for dataSource[%s], no reindexing jobs will be created", dataSource); + return Collections.emptyList(); + } + Interval adjustedTimelineInterval = applySkipOffset( new Interval(timeline.first().getInterval().getStart(), timeline.last().getInterval().getEnd()), jobParams.getScheduleStartTime() @@ -310,7 +313,7 @@ public List createCompactionJobs( return Collections.emptyList(); } - for (Interval reindexingInterval : intervals) { + for (Interval reindexingInterval : searchIntervals) { if (!reindexingInterval.overlaps(adjustedTimelineInterval)) { LOG.debug("Search interval[%s] does not overlap with data range[%s], skipping", reindexingInterval, adjustedTimelineInterval); @@ -345,6 +348,7 @@ public List createCompactionJobs( return allJobs; } + @VisibleForTesting protected CompactionJobTemplate createJobTemplateForInterval( InlineSchemaDataSourceCompactionConfig config ) @@ -352,6 +356,15 @@ protected CompactionJobTemplate createJobTemplateForInterval( return new CompactionConfigBasedJobTemplate(config, createCascadingFinalizer()); } + /** + * Clamps an interval to fit within the specified bounds by adjusting start and/or end times. + * If the interval extends beyond the bounds, it is trimmed to fit. Returns null if the + * resulting interval would be invalid (end before start). + * + * @param interval the interval to clamp + * @param bounds the bounds to clamp to + * @return the clamped interval, or null if the result would be invalid + */ @Nullable private Interval clampIntervalToBounds(Interval interval, Interval bounds) { @@ -383,6 +396,15 @@ private Interval clampIntervalToBounds(Interval interval, Interval bounds) return new Interval(start, end); } + /** + * Applies the configured skip offset to an interval by adjusting its end time. Uses either + * skipOffsetFromNow (relative to reference time) or skipOffsetFromLatest (relative to interval end). + * Returns null if the adjusted end would be before the interval start. + * + * @param interval the interval to adjust + * @param skipFromNowReferenceTime the reference time for skipOffsetFromNow calculation + * @return the interval with adjusted end time, or null if the result would be invalid + */ @Nullable private Interval applySkipOffset( Interval interval, @@ -418,11 +440,13 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() * then splits them at non-segment-granularity rule thresholds where safe to do so. *

* Algorithm: - * 1. Generate base timeline from segment granularity rules - * 2. Collect thresholds from all non-segment-granularity rules - * 3. For each base interval, find thresholds that fall within it - * 4. Align those thresholds to the interval's segment granularity - * 5. Split intervals at aligned thresholds + *

    + *
  1. Generate base timeline from segment granularity rules
  2. + *
  3. Collect thresholds from all non-segment-granularity rules
  4. + *
  5. For each base interval, find thresholds that fall within it
  6. + *
  7. Align those thresholds to the interval's segment granularity
  8. + *
  9. Split intervals at aligned thresholds
  10. + *
* * @param referenceTime the reference time for calculating period thresholds * @return list of split and aligned intervals, ordered from oldest to newest @@ -431,7 +455,7 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() List generateAlignedSearchIntervals(DateTime referenceTime) { // Step 1: Generate base timeline from segment granularity rules - List baseTimeline = generateBaseTimelineWithGranularities(referenceTime); + List baseTimeline = generateBaseSegmentGranularityAlignedTimeline(referenceTime); // Step 2: Collect thresholds from non-segment-granularity rules List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); @@ -484,19 +508,38 @@ List generateAlignedSearchIntervals(DateTime referenceTime) } /** - * Generates the base timeline with segment granularities tracked. + * Generates a base timeline aligned to segment granularities found in segment granularity rules and if necessary, + * the default granularity for the supervisor. *

- * If segment granularity rules exist, uses them to build the base timeline. - * If not, uses the smallest period from non-segment-granularity rules with the default granularity. + * Algorithm: + *

    + *
  1. If no segment granularity rules exist: + *
      + *
    1. Find the most recent threshold from non-segment-granularity rules
    2. + *
    3. Use the default granularity to align an interval from [-inf, most recent threshold)
    4. + *
    + *
  2. + *
  3. If segment granularity rules exist: + *
      + *
    1. Sort rules by period from longest to shortest (oldest to most recent threshold)
    2. + *
    3. Create intervals for each rule, adjusting the interval end to be aligned to the rule's segment granularity
    4. + *
    5. If non-segment-granularity thresholds exist that are more recent than the most recent segment granularity rule's end: + *
        + *
      1. Prepend an interval from [most recent segment granularity end, most recent non-segment-granularity threshold)
      2. + *
      + *
    6. + *
    + *
  4. + *
*/ - private List generateBaseTimelineWithGranularities(DateTime referenceTime) + private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) { + // Collect all rules upfront for efficient reuse List segmentGranRules = ruleProvider.getSegmentGranularityRules(); + List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); if (segmentGranRules.isEmpty()) { // No segment granularity rules - use default granularity with smallest period from other rules - List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); - if (nonSegmentGranThresholds.isEmpty()) { throw new IAE( "CascadingReindexingTemplate requires at least one reindexing rule " @@ -508,9 +551,10 @@ private List generateBaseTimelineWithGranularities(Date DateTime mostRecentThreshold = Collections.max(nonSegmentGranThresholds); DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentThreshold); - LOG.info( - "No segment granularity rules found. Creating base interval with default granularity [%s] " - + "and threshold [%s] (aligned: [%s])", + LOG.debug( + "No segment granularity rules found for cascading supervisor[%s]. Creating base interval with " + + "default granularity [%s] and threshold [%s] (aligned: [%s])", + dataSource, defaultSegmentGranularity, mostRecentThreshold, alignedEnd @@ -558,7 +602,6 @@ private List generateBaseTimelineWithGranularities(Date // Check if we need to prepend an interval for non-segment-gran rules that are more recent // than the most recent segment gran rule - List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); if (!nonSegmentGranThresholds.isEmpty()) { DateTime mostRecentNonSegmentGranThreshold = Collections.max(nonSegmentGranThresholds); DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).interval.getEnd(); @@ -567,19 +610,30 @@ private List generateBaseTimelineWithGranularities(Date // Need to prepend an interval for more recent non-segment-gran rules DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); - LOG.info( - "Most recent non-segment-gran threshold [%s] is after most recent segment gran interval end [%s]. " - + "Prepending interval with default granularity [%s] (aligned end: [%s])", - mostRecentNonSegmentGranThreshold, - mostRecentSegmentGranEnd, - defaultSegmentGranularity, - alignedEnd - ); - - baseTimeline.add(new IntervalWithGranularity( - new Interval(mostRecentSegmentGranEnd, alignedEnd), - defaultSegmentGranularity - )); + if (alignedEnd.isBefore(mostRecentSegmentGranEnd) || alignedEnd.isEqual(mostRecentSegmentGranEnd)) { + LOG.debug( + "Most recent non-segment-gran threshold [%s] aligns to [%s], which is not after " + + "most recent segment gran end [%s]. No prepended interval needed.", + mostRecentNonSegmentGranThreshold, + alignedEnd, + mostRecentSegmentGranEnd + ); + return baseTimeline; + } else { + LOG.debug( + "Most recent non-segment-gran threshold [%s] is after most recent segment gran interval end [%s]. " + + "Prepending interval with default granularity [%s] (aligned end: [%s])", + mostRecentNonSegmentGranThreshold, + mostRecentSegmentGranEnd, + defaultSegmentGranularity, + alignedEnd + ); + + baseTimeline.add(new IntervalWithGranularity( + new Interval(mostRecentSegmentGranEnd, alignedEnd), + defaultSegmentGranularity + )); + } } } @@ -591,70 +645,10 @@ private List generateBaseTimelineWithGranularities(Date */ private List collectNonSegmentGranularityThresholds(DateTime referenceTime) { - List thresholds = new ArrayList<>(); - - // Collect from all rule types - ruleProvider.getMetricsRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getDimensionsRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getIOConfigRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getProjectionRules().stream() + return ruleProvider.streamAllRules() + .filter(rule -> !(rule instanceof ReindexingSegmentGranularityRule)) .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getQueryGranularityRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getTuningConfigRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - ruleProvider.getDeletionRules().stream() - .map(rule -> referenceTime.minus(rule.getOlderThan())) - .forEach(thresholds::add); - - return thresholds; - } - - /** - * Helper class to track an interval with its associated segment granularity. - */ - private static class IntervalWithGranularity - { - final Interval interval; - final Granularity granularity; - - IntervalWithGranularity(Interval interval, Granularity granularity) - { - this.interval = interval; - this.granularity = granularity; - } - } - - @Deprecated - private List generateSearchIntervals(List sortedPeriods, DateTime referenceTime) - { - List intervals = new ArrayList<>(sortedPeriods.size()); - for (int i = 0; i < sortedPeriods.size(); i++) { - DateTime end = referenceTime.minus(sortedPeriods.get(i)); - DateTime start; - if (i + 1 < sortedPeriods.size()) { - start = referenceTime.minus(sortedPeriods.get(i + 1)); - } else { - start = DateTimes.MIN; - } - intervals.add(new Interval(start, end)); - } - return intervals; + .collect(Collectors.toList()); } private List createJobsForSearchInterval( @@ -676,6 +670,21 @@ public CompactionState toCompactionState() throw new UnsupportedOperationException("CascadingReindexingTemplate cannot be transformed to a CompactionState object"); } + /** + * Helper class to track an interval with its associated segment granularity. + */ + private static class IntervalWithGranularity + { + final Interval interval; + final Granularity granularity; + + IntervalWithGranularity(Interval interval, Granularity granularity) + { + this.interval = interval; + this.granularity = granularity; + } + } + // Legacy fields from DataSourceCompactionConfig that are not used by this template @Nullable diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index d82e1c09f3ec..499bb1058d7b 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -31,6 +31,7 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingMetricsRule; +import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; @@ -199,7 +200,7 @@ public void test_createCompactionJobs_simple() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -211,10 +212,11 @@ public void test_createCompactionJobs_simple() List processedIntervals = template.getProcessedIntervals(); Assert.assertEquals(2, processedIntervals.size()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(10), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + // Intervals are now in chronological order (oldest first) + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(10), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -224,7 +226,7 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_skipAllOfTime() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -245,7 +247,7 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_trimsIntervalEnd( { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -257,10 +259,11 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_trimsIntervalEnd( List processedIntervals = template.getProcessedIntervals(); Assert.assertEquals(2, processedIntervals.size()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(15), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + // Intervals are now in chronological order (oldest first) + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(15), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -270,7 +273,7 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_eliminatesInterva { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -293,7 +296,7 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_skipAllOfTime() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -314,7 +317,7 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_trimsIntervalEnd() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -326,10 +329,11 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_trimsIntervalEnd() List processedIntervals = template.getProcessedIntervals(); Assert.assertEquals(2, processedIntervals.size()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(20), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getEnd()); + // Intervals are now in chronological order (oldest first) + Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); + Assert.assertEquals(referenceTime.minusDays(20), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -339,7 +343,7 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); - ReindexingRuleProvider mockProvider = createMockProvider(referenceTime, List.of(Period.days(7), Period.days(30))); + ReindexingRuleProvider mockProvider = createMockProvider(List.of(Period.days(7), Period.days(30))); CompactionJobParams mockParams = createMockParams(referenceTime, timeline); DruidInputSource mockSource = createMockSource(); @@ -711,19 +715,25 @@ private SegmentTimeline createTestTimeline(DateTime start, DateTime end) return SegmentTimeline.forSegments(Collections.singletonList(segment)); } - private ReindexingRuleProvider createMockProvider(DateTime referenceTime, List periods) + private ReindexingRuleProvider createMockProvider(List periods) { - ReindexingSegmentGranularityRule mockGranularityRule = new ReindexingSegmentGranularityRule( - "test-rule", - null, - Period.days(1), - Granularities.HOUR - ); + // Create segment granularity rules for each period + List segmentGranularityRules = new ArrayList<>(); + for (int i = 0; i < periods.size(); i++) { + segmentGranularityRules.add(new ReindexingSegmentGranularityRule( + "segment-gran-rule-" + i, + null, + periods.get(i), + Granularities.HOUR + )); + } ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.expect(mockProvider.isReady()).andReturn(true); - EasyMock.expect(mockProvider.getCondensedAndSortedPeriods(referenceTime)).andReturn(periods); - EasyMock.expect(mockProvider.getSegmentGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(mockGranularityRule).anyTimes(); + EasyMock.expect(mockProvider.getSegmentGranularityRules()).andReturn(segmentGranularityRules).anyTimes(); + // Return a fresh stream on each call to avoid "stream has already been operated upon or closed" errors + EasyMock.expect(mockProvider.streamAllRules()).andAnswer(() -> segmentGranularityRules.stream().map(r -> (ReindexingRule) r)).anyTimes(); + EasyMock.expect(mockProvider.getSegmentGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(segmentGranularityRules.get(0)).anyTimes(); EasyMock.expect(mockProvider.getQueryGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getMetricsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getDimensionsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index cd41fe7facd9..a893e6e8259d 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -22,17 +22,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import org.joda.time.DateTime; -import org.joda.time.Duration; import org.joda.time.Interval; -import org.joda.time.Period; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.Collections; -import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; /** * A meta-provider that composes multiple {@link ReindexingRuleProvider}s with first-wins semantics. @@ -131,21 +126,6 @@ public boolean isReady() return providers.stream().allMatch(ReindexingRuleProvider::isReady); } - @Override - @Nonnull - public List getCondensedAndSortedPeriods(DateTime referenceTime) - { - // Collect all unique periods from all providers, sorted ascending - return providers.stream() - .flatMap(p -> p.getCondensedAndSortedPeriods(referenceTime).stream()) - .distinct() - .sorted(Comparator.comparingLong(period -> { - DateTime endTime = referenceTime.plus(period); - return new Duration(referenceTime, endTime).getMillis(); - })) - .collect(Collectors.toList()); - } - @Override public List getDeletionRules() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index be43dee22d8c..f6a76c3f2623 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -23,19 +23,14 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.common.config.Configs; import org.joda.time.DateTime; -import org.joda.time.Duration; import org.joda.time.Interval; -import org.joda.time.Period; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; /** * Rule provider that returns a static list of rules defined inline in the configuration. @@ -189,31 +184,6 @@ public List getTuningConfigRules() return reindexingTuningConfigRules; } - @Override - @Nonnull - public List getCondensedAndSortedPeriods(DateTime referenceTime) - { - return Stream.of( - reindexingDeletionRules, - reindexingMetricsRules, - reindexingDimensionsRules, - reindexingIOConfigRules, - reindexingProjectionRules, - reindexingSegmentGranularityRules, - reindexingQueryGranularityRules, - reindexingTuningConfigRules - ) - .flatMap(List::stream) - .map(ReindexingRule::getOlderThan) - .distinct() - .sorted(Comparator.comparingLong(period -> { - DateTime endTime = referenceTime.plus(period); - return new Duration(referenceTime, endTime).getMillis(); - })) - .collect(Collectors.toList()); - - } - @Override public List getDeletionRules(Interval interval, DateTime referenceTime) { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 9c6a834acd7c..4d5649af431a 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -23,11 +23,10 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import org.joda.time.DateTime; import org.joda.time.Interval; -import org.joda.time.Period; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.List; +import java.util.stream.Stream; /** * Provides compaction rules for different aspects of reindexing configuration. @@ -71,15 +70,6 @@ default boolean isReady() return true; } - /** - * Returns all unique periods used by the rules provided by this provider, condensed and sorted in ascending order. - *

- * Ascending order means from shortest to longest period. For example, [P1D, P7D, P30D]. - *

- */ - @Nonnull - List getCondensedAndSortedPeriods(DateTime referenceTime); - /** * Returns all reindexing deletion rules that apply to the given interval. *

@@ -235,4 +225,27 @@ default boolean isReady() * Returns ALL reindexing tuning config rules. */ List getTuningConfigRules(); + + /** + * Returns a stream of all reindexing rules across all types. + *

+ * This provides a flexible way to filter, map, and process rules without needing + * specific methods for every possible combination. For example, to get all non-segment-granularity + * rules, you can filter: {@code streamAllRules().filter(rule -> !(rule instanceof ReindexingSegmentGranularityRule))} + * + * @return a stream of all rules from all rule types + */ + default Stream streamAllRules() + { + return Stream.of( + getMetricsRules().stream(), + getDimensionsRules().stream(), + getIOConfigRules().stream(), + getProjectionRules().stream(), + getQueryGranularityRules().stream(), + getTuningConfigRules().stream(), + getDeletionRules().stream(), + getSegmentGranularityRules().stream() + ).flatMap(s -> s); + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 4ba9df6714ba..96511cfa480a 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -167,30 +167,6 @@ public void test_getQueryGranularityRules_compositingBehavior() ); } - @Test - public void test_getCondensedAndSortedPeriods_mergesFromAllProviders() - { - ReindexingDeletionRule rule1 = createFilterRule("rule1", Period.days(7)); - ReindexingDeletionRule rule2 = createFilterRule("rule2", Period.months(1)); - ReindexingDeletionRule rule3 = createFilterRule("rule3", Period.days(7)); // Duplicate period - - ReindexingRuleProvider provider1 = InlineReindexingRuleProvider.builder() - .deletionRules(ImmutableList.of(rule1)).build(); - ReindexingRuleProvider provider2 = InlineReindexingRuleProvider.builder() - .deletionRules(ImmutableList.of(rule2, rule3)).build(); - - ComposingReindexingRuleProvider composing = new ComposingReindexingRuleProvider( - ImmutableList.of(provider1, provider2) - ); - - List result = composing.getCondensedAndSortedPeriods(REFERENCE_TIME); - - Assert.assertEquals(2, result.size()); - Assert.assertEquals(Period.days(7), result.get(0)); - Assert.assertEquals(Period.months(1), result.get(1)); - } - - @Test public void test_getMetricsRules_compositingBehavior() { diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 71d86222bfc1..8effac0f8a6f 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -80,38 +80,6 @@ public void test_constructor_nullListsDefaultToEmpty() Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); } - @Test - public void test_getCondensedAndSortedPeriods_returnsDistinctSortedPeriods() - { - ReindexingDeletionRule filter30d = createFilterRule("f1", Period.days(30)); - ReindexingDeletionRule filter60d = createFilterRule("f2", Period.days(60)); - ReindexingSegmentGranularityRule gran30d = createSegmentGranularityRule("g1", Period.days(30)); // Duplicate P30D - ReindexingSegmentGranularityRule gran90d = createSegmentGranularityRule("g2", Period.days(90)); - - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .deletionRules(ImmutableList.of(filter30d, filter60d)) - .segmentGranularityRules(ImmutableList.of(gran30d, gran90d)) - .build(); - - List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); - - Assert.assertEquals(3, periods.size()); - - Assert.assertEquals(Period.days(30), periods.get(0)); - Assert.assertEquals(Period.days(60), periods.get(1)); - Assert.assertEquals(Period.days(90), periods.get(2)); - } - - @Test - public void test_getCondensedAndSortedPeriods_withEmptyRules_returnsEmpty() - { - InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); - - List periods = provider.getCondensedAndSortedPeriods(REFERENCE_TIME); - - Assert.assertTrue(periods.isEmpty()); - } - @Test public void test_reindexingRules_validateAdditivity() { From b58f026cdc8ed37d93e4db2086e6c8cd8f6bbd5b Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 1 Feb 2026 21:28:34 -0600 Subject: [PATCH 40/90] fix import --- .../server/compaction/ReindexingQueryGranularityRule.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java index fa176926951e..6721f376a12e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import io.github.resilience4j.core.lang.NonNull; import org.apache.druid.java.util.common.granularity.Granularity; import org.joda.time.Period; @@ -59,7 +58,7 @@ public ReindexingQueryGranularityRule( @JsonProperty("id") @Nonnull String id, @JsonProperty("description") @Nullable String description, @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("queryGranularity") @NonNull Granularity queryGranularity, + @JsonProperty("queryGranularity") @Nonnull Granularity queryGranularity, @JsonProperty("rollup") @Nonnull Boolean rollup ) { From f8d3be8379acd67d66f18b364f0c5fd57f1ad1a0 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 2 Feb 2026 08:33:45 -0600 Subject: [PATCH 41/90] Fixing imports --- .../indexing/compact/CascadingReindexingTemplateTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 499bb1058d7b..22b16030d624 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -40,7 +40,6 @@ import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentTimeline; import org.easymock.EasyMock; -import org.jetbrains.annotations.Nullable; import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; @@ -48,6 +47,7 @@ import org.junit.Before; import org.junit.Test; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -683,7 +683,8 @@ public String getType() } @Override - public @Nullable Granularity getSegmentGranularity() + @Nullable + public Granularity getSegmentGranularity() { return null; } From 49bd2fc13c045e0118be06e4ab22b832eca3188a Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 2 Feb 2026 08:42:18 -0600 Subject: [PATCH 42/90] fix a test --- .../server/compaction/InlineReindexingRuleProviderTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 8effac0f8a6f..9a3ab21023d3 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -229,7 +229,7 @@ private void testNonAdditivity( T rule90d = ruleFactory.apply(ruleTypeName + "-90d", Period.days(90)); InlineReindexingRuleProvider.Builder builder = InlineReindexingRuleProvider.builder(); - builderSetter.apply(builder, ImmutableList.of(rule30d, rule60d, rule90d)); + builder = builderSetter.apply(builder, ImmutableList.of(rule30d, rule60d, rule90d)); InlineReindexingRuleProvider provider = builder.build(); Assert.assertNull( From c3197ba94e065fc6f1ca8b8b510055423f5f147b Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 2 Feb 2026 09:00:38 -0600 Subject: [PATCH 43/90] cleanup --- .../compact/CascadingReindexingTemplate.java | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 238ba5872076..60e50fba3a23 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -304,6 +304,7 @@ public List createCompactionJobs( return Collections.emptyList(); } + // Adjust timeline interval by applying user defined skip offset (if any exists) Interval adjustedTimelineInterval = applySkipOffset( new Interval(timeline.first().getInterval().getStart(), timeline.last().getInterval().getEnd()), jobParams.getScheduleStartTime() @@ -316,13 +317,14 @@ public List createCompactionJobs( for (Interval reindexingInterval : searchIntervals) { if (!reindexingInterval.overlaps(adjustedTimelineInterval)) { + // No underlying data exists to reindex for this interval LOG.debug("Search interval[%s] does not overlap with data range[%s], skipping", reindexingInterval, adjustedTimelineInterval); continue; } reindexingInterval = clampIntervalToBounds(reindexingInterval, adjustedTimelineInterval); if (reindexingInterval == null) { - LOG.warn("Clamped reindexing interval is empty after applying bounds, meaning no search interval exists. Skipping."); + LOG.warn("Clamped reindexing interval is invalid or empty after applying bounds, meaning no search interval exists. Skipping."); continue; } @@ -332,7 +334,7 @@ public List createCompactionJobs( int ruleCount = configBuilder.applyTo(builder); if (ruleCount > 0) { - LOG.info("Creating reindexing jobs for interval[%s] with [%d] rules selected", reindexingInterval, ruleCount); + LOG.debug("Creating reindexing jobs for interval[%s] with [%d] rules selected", reindexingInterval, ruleCount); allJobs.addAll( createJobsForSearchInterval( createJobTemplateForInterval(builder.build()), @@ -342,7 +344,7 @@ public List createCompactionJobs( ) ); } else { - LOG.info("No applicable reindexing rules found for interval[%s]", reindexingInterval); + LOG.debug("No applicable reindexing rules found for interval[%s]", reindexingInterval); } } return allJobs; @@ -363,7 +365,7 @@ protected CompactionJobTemplate createJobTemplateForInterval( * * @param interval the interval to clamp * @param bounds the bounds to clamp to - * @return the clamped interval, or null if the result would be invalid + * @return the clamped interval, or null if the result would be invalid or empty */ @Nullable private Interval clampIntervalToBounds(Interval interval, Interval bounds) @@ -389,7 +391,7 @@ private Interval clampIntervalToBounds(Interval interval, Interval bounds) end = bounds.getEnd(); } - if (end.isBefore(start)) { + if (end.isBefore(start) || end.isEqual(start)) { return null; } @@ -441,11 +443,15 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() *

* Algorithm: *

    - *
  1. Generate base timeline from segment granularity rules
  2. - *
  3. Collect thresholds from all non-segment-granularity rules
  4. - *
  5. For each base interval, find thresholds that fall within it
  6. - *
  7. Align those thresholds to the interval's segment granularity
  8. - *
  9. Split intervals at aligned thresholds
  10. + *
  11. Generate base timeline from segment granularity rules with interval boundaries aligned with segment granularity of the underlying rules
  12. + *
  13. Collect olderThan application thresholds from all non-segment-granularity rules
  14. + *
  15. For each interval in the base timeline: + *
      + *
    • find thresholds for non-segment granularity rules that fall within it
    • + *
    • Align those thresholds to the interval's segment granularity
    • + *
    • Split base intervals at aligned thresholds
    • + *
    + *
  16. Return the timeline of non-overlapping intervals split for most precise possible rule application (due to segment gran alignment, sometimes rules will be applied later than their explicitly defined period)
  17. *
* * @param referenceTime the reference time for calculating period thresholds @@ -454,19 +460,15 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() */ List generateAlignedSearchIntervals(DateTime referenceTime) { - // Step 1: Generate base timeline from segment granularity rules List baseTimeline = generateBaseSegmentGranularityAlignedTimeline(referenceTime); - // Step 2: Collect thresholds from non-segment-granularity rules List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); - // Step 3: For each base interval, collect and apply split points List finalIntervals = new ArrayList<>(); for (IntervalWithGranularity baseInterval : baseTimeline) { List splitPoints = new ArrayList<>(); - // Find thresholds that fall within this base interval for (DateTime threshold : nonSegmentGranThresholds) { if (threshold.isAfter(baseInterval.interval.getStart()) && threshold.isBefore(baseInterval.interval.getEnd())) { @@ -482,7 +484,6 @@ List generateAlignedSearchIntervals(DateTime referenceTime) } } - // Sort and deduplicate split points splitPoints = splitPoints.stream() .distinct() .sorted() @@ -499,7 +500,6 @@ List generateAlignedSearchIntervals(DateTime referenceTime) finalIntervals.add(new Interval(start, splitPoint)); start = splitPoint; } - // Add the final segment finalIntervals.add(new Interval(start, baseInterval.interval.getEnd())); } } @@ -534,12 +534,11 @@ List generateAlignedSearchIntervals(DateTime referenceTime) */ private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) { - // Collect all rules upfront for efficient reuse List segmentGranRules = ruleProvider.getSegmentGranularityRules(); List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); if (segmentGranRules.isEmpty()) { - // No segment granularity rules - use default granularity with smallest period from other rules + // No segment granularity rules - use default granularity with the smallest period from other rules if (nonSegmentGranThresholds.isEmpty()) { throw new IAE( "CascadingReindexingTemplate requires at least one reindexing rule " @@ -570,7 +569,7 @@ private List generateBaseSegmentGranularityAlignedTimel List sortedRules = segmentGranRules.stream() .sorted(Comparator.comparingLong(rule -> { DateTime threshold = referenceTime.minus(rule.getOlderThan()); - return threshold.getMillis(); // Ascending = oldest first + return threshold.getMillis(); })) .collect(Collectors.toList()); @@ -607,7 +606,6 @@ private List generateBaseSegmentGranularityAlignedTimel DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).interval.getEnd(); if (mostRecentNonSegmentGranThreshold.isAfter(mostRecentSegmentGranEnd)) { - // Need to prepend an interval for more recent non-segment-gran rules DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); if (alignedEnd.isBefore(mostRecentSegmentGranEnd) || alignedEnd.isEqual(mostRecentSegmentGranEnd)) { From 457d9129a62cc16ab773677b97bcc212ae0cb904 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 4 Feb 2026 14:06:20 -0600 Subject: [PATCH 44/90] fix imports after merging master --- .../testing/embedded/compact/CompactionSupervisorTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 6fdc0f82a9f0..a1b7e5337fce 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -27,8 +27,8 @@ import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexer.partitions.DimensionRangePartitionsSpec; import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; -import org.apache.druid.indexer.partitions.PartitionsSpec; import org.apache.druid.indexer.partitions.HashedPartitionsSpec; +import org.apache.druid.indexer.partitions.PartitionsSpec; import org.apache.druid.indexing.common.task.IndexTask; import org.apache.druid.indexing.common.task.TaskBuilder; import org.apache.druid.indexing.compact.CascadingReindexingTemplate; @@ -43,23 +43,22 @@ import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.query.DruidMetrics; import org.apache.druid.query.expression.TestExprMacroTable; -import org.apache.druid.query.filter.SelectorDimFilter; -import org.apache.druid.query.http.ClientSqlQuery; import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.query.http.ClientSqlQuery; import org.apache.druid.rpc.UpdateResponse; import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingDeletionRule; import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; import org.apache.druid.server.compaction.ReindexingTuningConfigRule; -import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.ClusterCompactionConfig; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; From b44882bf621e37bd4bb9bd9c5118e5d43a9bfab5 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 4 Feb 2026 14:27:40 -0600 Subject: [PATCH 45/90] fixup test file --- .../compact/CompactionSupervisorTest.java | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index a1b7e5337fce..02e6463f3f93 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -245,13 +245,18 @@ public void test_compaction_withPersistLastCompactionStateFalse_storesOnlyFinger verifySegmentsHaveNullLastCompactionStateAndNonNullFingerprint(); } - @Test - public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompactionRules() + @MethodSource("getEngine") + @ParameterizedTest(name = "compactionEngine={0}") + public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompactionRules(CompactionEngine compactionEngine) { - // We eventually want to run with parameterized test for both engines but right now using RANGE partitioning and filtering - // out all rows with native engine cant handle right now. - CompactionEngine compactionEngine = CompactionEngine.MSQ; - configureCompaction(compactionEngine); + // Configure cluster with storeCompactionStatePerSegment=false + final UpdateResponse updateResponse = cluster.callApi().onLeaderOverlord( + o -> o.updateClusterCompactionConfig( + new ClusterCompactionConfig(1.0, 100, null, true, compactionEngine, false) + ) + ); + Assertions.assertTrue(updateResponse.isSuccess()); + DateTime now = DateTimes.nowUtc(); @@ -337,7 +342,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac Assertions.assertEquals(4, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); Assertions.assertEquals(5, getNumSegmentsWith(Granularities.HOUR)); - Assertions.assertEquals(7, getNumSegmentsWith(Granularities.DAY)); + Assertions.assertEquals(4, getNumSegmentsWith(Granularities.DAY)); verifyEventCountOlderThan(Period.days(7), "item", "hat", 0); } @@ -516,7 +521,8 @@ public void test_compactionWithTransformFilteringAllRows_createsTombstones( .withTransformSpec( // This filter drops all rows: expression "false" always evaluates to false new CompactionTransformSpec( - new NotDimFilter(new SelectorDimFilter("item", "shirt", null)) + new NotDimFilter(new SelectorDimFilter("item", "shirt", null)), + null ) ); From 846ff7c5c5318367c307d9e993dc9ec108851b28 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 4 Feb 2026 17:47:17 -0600 Subject: [PATCH 46/90] test updates --- .../compact/CompactionSupervisorTest.java | 105 ++- .../task/CompactionTaskParallelRunTest.java | 6 +- .../CascadingReindexingTemplateTest.java | 622 +++++++++++++++--- 3 files changed, 588 insertions(+), 145 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 02e6463f3f93..4a9cc45d6dea 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -430,59 +430,6 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec verifyNoRowsWithNestedValue("extraInfo", "fieldA", "valueA"); } - private int getTotalRowCount() - { - String sql = StringUtils.format("SELECT COUNT(*) as cnt FROM \"%s\"", dataSource); - String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); - List> rows = JacksonUtils.readValue( - new DefaultObjectMapper(), - result.getBytes(StandardCharsets.UTF_8), - new TypeReference<>() {} - ); - return ((Number) rows.get(0).get("cnt")).intValue(); - } - - private void verifyNoRowsWithNestedValue(String nestedColumn, String field, String value) - { - String sql = StringUtils.format( - "SELECT COUNT(*) as cnt FROM \"%s\" WHERE json_value(%s, '$.%s') = '%s'", - dataSource, - nestedColumn, - field, - value - ); - String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); - List> rows = JacksonUtils.readValue( - new DefaultObjectMapper(), - result.getBytes(StandardCharsets.UTF_8), - new TypeReference<>() {} - ); - Assertions.assertEquals( - 0, - ((Number) rows.get(0).get("cnt")).intValue(), - StringUtils.format("Expected no rows where %s.%s = '%s'", nestedColumn, field, value) - ); - } - - - private String generateEventsInInterval(Interval interval, int numEvents, long spacingMillis) - { - List events = new ArrayList<>(); - - for (int i = 1; i <= numEvents; i++) { - DateTime eventTime = interval.getStart().plus(spacingMillis * i); - if (eventTime.isAfter(interval.getEnd())) { - throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); - } - String item = i % 2 == 0 ? "hat" : "shirt"; - int metricValue = 100 + i * 5; - events.add(eventTime + "," + item + "," + metricValue); - } - - return String.join("\n", events); - } - - /** * Tests that when a compaction task filters out all rows using a transform spec, * tombstones are created to properly drop the old segments. This test covers both @@ -598,6 +545,58 @@ public void test_compactionWithTransformFilteringAllRows_createsTombstones( ); } + private int getTotalRowCount() + { + String sql = StringUtils.format("SELECT COUNT(*) as cnt FROM \"%s\"", dataSource); + String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); + List> rows = JacksonUtils.readValue( + new DefaultObjectMapper(), + result.getBytes(StandardCharsets.UTF_8), + new TypeReference<>() {} + ); + return ((Number) rows.get(0).get("cnt")).intValue(); + } + + private void verifyNoRowsWithNestedValue(String nestedColumn, String field, String value) + { + String sql = StringUtils.format( + "SELECT COUNT(*) as cnt FROM \"%s\" WHERE json_value(%s, '$.%s') = '%s'", + dataSource, + nestedColumn, + field, + value + ); + String result = cluster.callApi().onAnyBroker(b -> b.submitSqlQuery(new ClientSqlQuery(sql, null, false, false, false, null, null))); + List> rows = JacksonUtils.readValue( + new DefaultObjectMapper(), + result.getBytes(StandardCharsets.UTF_8), + new TypeReference<>() {} + ); + Assertions.assertEquals( + 0, + ((Number) rows.get(0).get("cnt")).intValue(), + StringUtils.format("Expected no rows where %s.%s = '%s'", nestedColumn, field, value) + ); + } + + + private String generateEventsInInterval(Interval interval, int numEvents, long spacingMillis) + { + List events = new ArrayList<>(); + + for (int i = 1; i <= numEvents; i++) { + DateTime eventTime = interval.getStart().plus(spacingMillis * i); + if (eventTime.isAfter(interval.getEnd())) { + throw new IAE("Interval cannot fit [%d] events with spacing of [%d] millis", numEvents, spacingMillis); + } + String item = i % 2 == 0 ? "hat" : "shirt"; + int metricValue = 100 + i * 5; + events.add(eventTime + "," + item + "," + metricValue); + } + + return String.join("\n", events); + } + private void verifySegmentsHaveNullLastCompactionStateAndNonNullFingerprint() { overlord diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java index c72619ffa4cf..7217e532c3ef 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/common/task/CompactionTaskParallelRunTest.java @@ -1029,7 +1029,8 @@ public void testRunParallelWithRangePartitioningFilteringAllRows() throws Except true )) .transformSpec(new CompactionTransformSpec( - new SelectorDimFilter("dim", "nonexistent_value", null) // Filters out all rows + new SelectorDimFilter("dim", "nonexistent_value", null), // Filters out all rows + null )) .build(); @@ -1076,7 +1077,8 @@ public void testRunParallelRangePartitioningFilterAllRowsReplaceLegacyMode() thr true )) .transformSpec(new CompactionTransformSpec( - new SelectorDimFilter("dim", "nonexistent_value", null) // Filters all rows + new SelectorDimFilter("dim", "nonexistent_value", null), // Filters all rows + null )) .build(); diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 22b16030d624..022a2aea082e 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -361,16 +361,43 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() EasyMock.verify(mockProvider, mockParams, mockSource); } + /** + * TEST: Basic timeline construction with multiple segment granularity rules + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P7D→HOUR, P1M→DAY, P3M→MONTH
  • + *
  • Other Rules: None
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: None created
  2. + *
  3. Initial Timeline: + *
      + *
    • P3M → MONTH: Raw 2024-10-29T16:15 → Aligned 2024-10-01T00:00
    • + *
    • P1M → DAY: Raw 2024-12-29T16:15 → Aligned 2024-12-29T00:00
    • + *
    • P7D → HOUR: Raw 2025-01-22T16:15 → Aligned 2025-01-22T16:00
    • + *
    + *
  4. + *
  5. Timeline Splits: None (no non-segment-gran rules)
  6. + *
+ *

+ * EXPECTED OUTPUT: 3 intervals + *

    + *
  1. [-∞, 2024-10-01T00:00:00) - MONTH
  2. + *
  3. [2024-10-01T00:00:00, 2024-12-29T00:00:00) - DAY
  4. + *
  5. [2024-12-29T00:00:00, 2025-01-22T16:00:00) - HOUR
  6. + *
+ */ @Test public void test_generateAlignedSearchIntervals_withGranularityAlignment() { - // Reference time: 2025-01-29T16:15 DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - // Create rules with different periods and granularities - // P7D with HOUR granularity -> raw end: 2025-01-22T16:15 -> aligned: 2025-01-22T16:00 - // P1M with DAY granularity -> raw end: 2024-12-29T16:15 -> aligned: 2024-12-29T00:00 - // P3M with MONTH granularity -> raw end: 2024-10-29T16:15 -> aligned: 2024-10-01T00:00 ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), @@ -393,43 +420,60 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() List intervals = template.generateAlignedSearchIntervals(referenceTime); - // Expected 3 intervals, ordered from oldest to newest: - // 1. [-inf, 2024-10-01T00:00) - P3M rule with MONTH granularity - // 2. [2024-10-01T00:00, 2024-12-29T00:00) - P1M rule with DAY granularity - // 3. [2024-12-29T00:00, 2025-01-22T16:00) - P7D rule with HOUR granularity - Assert.assertEquals(3, intervals.size()); - // Interval 1: oldest Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); - // Interval 2: middle Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); - // Interval 3: most recent Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(2).getEnd()); } + /** + * TEST: Timeline splitting by non-segment-granularity rules (metrics rules) + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P7D→HOUR, P1M→DAY, P3M→MONTH
  • + *
  • Other Rules: P8D-metrics, P14D-metrics, P45D-metrics, P100D-metrics
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: None (smallest segment gran rule P7D is finer than all metrics rules)
  2. + *
  3. Initial Timeline: [-∞, 2024-10-01) MONTH, [2024-10-01, 2024-12-29) DAY, [2024-12-29, 2025-01-22T16:00) HOUR
  4. + *
  5. Timeline Splits: + *
      + *
    • P100D → Raw 2024-10-21T16:15 → Falls in DAY interval → Aligned 2024-10-21T00:00 → CREATES SPLIT
    • + *
    • P45D → Raw 2024-12-15T16:15 → Falls in DAY interval → Aligned 2024-12-15T00:00 → CREATES SPLIT
    • + *
    • P14D → Raw 2025-01-15T16:15 → Falls in HOUR interval → Aligned 2025-01-15T16:00 → CREATES SPLIT
    • + *
    • P8D → Raw 2025-01-21T16:15 → Falls in HOUR interval → Aligned 2025-01-21T16:00 → CREATES SPLIT
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 7 intervals + *

    + *
  1. [-∞, 2024-10-01T00:00:00) - MONTH
  2. + *
  3. [2024-10-01T00:00:00, 2024-10-21T00:00:00) - DAY
  4. + *
  5. [2024-10-21T00:00:00, 2024-12-15T00:00:00) - DAY
  6. + *
  7. [2024-12-15T00:00:00, 2024-12-29T00:00:00) - DAY
  8. + *
  9. [2024-12-29T00:00:00, 2025-01-15T16:00:00) - HOUR
  10. + *
  11. [2025-01-15T16:00:00, 2025-01-21T16:00:00) - HOUR
  12. + *
  13. [2025-01-21T16:00:00, 2025-01-22T16:00:00) - HOUR
  14. + *
+ */ @Test public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSplits() { - // Reference time: 2025-01-29T16:15 DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - // Segment granularity rules create base timeline: - // [-inf, 2024-10-01T00:00) MONTH - // [2024-10-01T00:00, 2024-12-29T00:00) DAY - // [2024-12-29T00:00, 2025-01-22T16:00) HOUR - // - // Non-segment-gran rules with thresholds: - // P8D -> 2025-01-21T16:15 -> falls in HOUR interval -> aligned: 2025-01-21T16:00 - // P14D -> 2025-01-15T16:15 -> falls in HOUR interval -> aligned: 2025-01-15T16:00 - // P45D -> 2024-12-15T16:15 -> falls in DAY interval -> aligned: 2024-12-15T00:00 - // P100D -> 2024-10-21T16:15 -> falls in MONTH interval -> aligned: 2024-10-01T00:00 (at boundary, no split) - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), @@ -470,56 +514,67 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl List intervals = template.generateAlignedSearchIntervals(referenceTime); - // Expected 7 intervals after splitting: - // 1. [-inf, 2024-10-01T00:00) - no split in MONTH interval - // 2. [2024-10-01T00:00, 2024-10-21T00:00) - split by P100D (falls in DAY interval!) - // 3. [2024-10-21T00:00, 2024-12-15T00:00) - between P100D and P45D - // 4. [2024-12-15T00:00, 2024-12-29T00:00) - split by P45D - // 5. [2024-12-29T00:00, 2025-01-15T16:00) - split by P14D - // 6. [2025-01-15T16:00, 2025-01-21T16:00) - between P14D and P8D - // 7. [2025-01-21T16:00, 2025-01-22T16:00) - split by P8D - Assert.assertEquals(7, intervals.size()); - // Interval 1: no split in MONTH interval Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); - // Interval 2: split by P100D (2024-10-21 falls in DAY granularity interval) Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(1).getEnd()); - // Interval 3: between P100D and P45D Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(2).getStart()); Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(2).getEnd()); - // Interval 4: split by P45D Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(3).getStart()); Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(3).getEnd()); - // Interval 5: split by P14D Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(4).getStart()); Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getEnd()); - // Interval 6: between P14D and P8D Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(5).getStart()); Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(5).getEnd()); - // Interval 7: split by P8D Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(6).getStart()); Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getEnd()); } + /** + * TEST: Timeline construction when NO segment granularity rules exist (Case A: default usage) + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: None
  • + *
  • Other Rules: P8D-metrics, P14D-metrics, P45D-metrics
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: Created P8D→DAY (Case A: no segment gran rules exist, use smallest rule period with default gran)
  2. + *
  3. Initial Timeline: [-∞, 2025-01-21T00:00) - DAY (from synthetic P8D rule)
  4. + *
  5. Timeline Splits: + *
      + *
    • P45D → Raw 2024-12-15T16:15 → Falls in DAY interval → Aligned 2024-12-15T00:00 → CREATES SPLIT
    • + *
    • P14D → Raw 2025-01-15T16:15 → Falls in DAY interval → Aligned 2025-01-15T00:00 → CREATES SPLIT
    • + *
    • P8D is now a segment gran rule (not processed as split)
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 3 intervals + *

    + *
  1. [-∞, 2024-12-15T00:00:00) - DAY
  2. + *
  3. [2024-12-15T00:00:00, 2025-01-15T00:00:00) - DAY
  4. + *
  5. [2025-01-15T00:00:00, 2025-01-21T00:00:00) - DAY
  6. + *
+ */ @Test public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() { - // Reference time: 2025-01-29T16:15 DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - // No segment granularity rules - only metrics rules - // P8D -> 2025-01-21T16:15 (smallest/most recent) - // P14D -> 2025-01-15T16:15 - // P45D -> 2024-12-15T16:15 ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .metricsRules(List.of( new ReindexingMetricsRule("metrics-8d", null, Period.days(8), new AggregatorFactory[0]), @@ -537,28 +592,11 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() null, null, null, - Granularities.DAY // default granularity + Granularities.DAY ); List intervals = template.generateAlignedSearchIntervals(referenceTime); - // Expected: One base interval [-inf, aligned(smallest period)) then split by other rules - // Smallest period is P8D -> threshold: 2025-01-21T16:15 - // Aligned to DAY: 2025-01-21T00:00 - // Then split by P14D (2025-01-15T00:00) and P45D (2024-12-15T00:00) - // - // Result: 4 intervals - // 1. [-inf, 2024-12-15T00:00) - // 2. [2024-12-15T00:00, 2025-01-15T00:00) - // 3. [2025-01-15T00:00, 2025-01-21T00:00) - // 4. [2025-01-21T00:00, 2025-01-21T00:00) - WAIT, this would be zero-length! - - // Actually, P14D and P45D both fall within the base interval [-inf, 2025-01-21T00:00) - // So they will split it: - // 1. [-inf, 2024-12-15T00:00) - before P45D - // 2. [2024-12-15T00:00, 2025-01-15T00:00) - between P45D and P14D - // 3. [2025-01-15T00:00, 2025-01-21T00:00) - after P14D, before base end - Assert.assertEquals(3, intervals.size()); Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); @@ -571,21 +609,51 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() Assert.assertEquals(DateTimes.of("2025-01-21T00:00:00Z"), intervals.get(2).getEnd()); } + /** + * TEST: Synthetic segment gran rule creation when rules are finer than smallest segment gran rule (Case B) + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1M→DAY, P3M→MONTH
  • + *
  • Other Rules: P7D-metrics, P14D-metrics, P21D-metrics (all finer than P1M!)
  • + *
  • Default Segment Granularity: HOUR
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: Created P7D→HOUR (Case B: P7D/P14D/P21D are finer than smallest segment gran rule P1M, use finest with default gran)
  2. + *
  3. Initial Timeline: + *
      + *
    • P3M → MONTH: Raw 2024-10-29T16:15 → Aligned 2024-10-01T00:00
    • + *
    • P1M → DAY: Raw 2024-12-29T16:15 → Aligned 2024-12-29T00:00
    • + *
    • P7D → HOUR (synthetic): Raw 2025-01-22T16:15 → Aligned 2025-01-22T16:00 (PREPENDED interval!)
    • + *
    + *
  4. + *
  5. Timeline Splits: + *
      + *
    • P21D → Raw 2025-01-08T16:15 → Falls in HOUR interval → Aligned 2025-01-08T16:00 → CREATES SPLIT
    • + *
    • P14D → Raw 2025-01-15T16:15 → Falls in HOUR interval → Aligned 2025-01-15T16:00 → CREATES SPLIT
    • + *
    • P7D is now a segment gran rule (not processed as split)
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 5 intervals + *

    + *
  1. [-∞, 2024-10-01T00:00:00) - MONTH
  2. + *
  3. [2024-10-01T00:00:00, 2024-12-29T00:00:00) - DAY
  4. + *
  5. [2024-12-29T00:00:00, 2025-01-08T16:00:00) - HOUR (prepended)
  6. + *
  7. [2025-01-08T16:00:00, 2025-01-15T16:00:00) - HOUR (prepended)
  8. + *
  9. [2025-01-15T16:00:00, 2025-01-22T16:00:00) - HOUR (prepended)
  10. + *
+ */ @Test public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmentGranRules() { - // Reference time: 2025-01-29T16:15 DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - // Segment granularity rules: - // P1M -> 2024-12-29T16:15 (most recent segment gran threshold) - // P3M -> 2024-10-29T16:15 - // - // Non-segment-gran rules (more recent than P1M!): - // P7D -> 2025-01-22T16:15 - // P14D -> 2025-01-15T16:15 - // P21D -> 2025-01-08T16:15 - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH), @@ -607,44 +675,418 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen null, null, null, - Granularities.HOUR // default for prepended interval + Granularities.HOUR ); List intervals = template.generateAlignedSearchIntervals(referenceTime); - // Expected base timeline: - // 1. [-inf, 2024-10-01T00:00) - P3M with MONTH gran - // 2. [2024-10-01T00:00, 2024-12-29T00:00) - P1M with DAY gran - // 3. [2024-12-29T00:00, 2025-01-22T16:00) - PREPENDED with HOUR gran (for P7D) - // - // Then split by P14D (2025-01-15T16:00) and P21D (2025-01-08T16:00) which fall in interval 3: - // 3a. [2024-12-29T00:00, 2025-01-08T16:00) - // 3b. [2025-01-08T16:00, 2025-01-15T16:00) - // 3c. [2025-01-15T16:00, 2025-01-22T16:00) - Assert.assertEquals(5, intervals.size()); - // Interval 1: oldest Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); - // Interval 2: middle Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); - // Interval 3a: prepended interval, split by P21D Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(2).getEnd()); - // Interval 3b: between P21D and P14D Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(3).getStart()); Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(3).getEnd()); - // Interval 3c: after P14D Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getStart()); Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(4).getEnd()); } + /** + * TEST: Comprehensive example demonstrating Case B, multiple segment gran rules, and timeline splits + *

+ * REFERENCE TIME: 2024-02-04T22:12:04.873Z (realistic messy timestamp) + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1Y→YEAR, P1M→MONTH, P7D→DAY
  • + *
  • Other Rules: P1D-metrics, P14D-metrics, P45D-metrics (P1D is finer than P7D!)
  • + *
  • Default Segment Granularity: HOUR
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: Created P1D→HOUR (Case B: P1D is finer than smallest segment gran rule P7D)
  2. + *
  3. Initial Timeline: + *
      + *
    • P1Y → YEAR: Raw 2023-02-04T22:12:04.873 → Aligned 2023-01-01T00:00:00
    • + *
    • P1M → MONTH: Raw 2024-01-04T22:12:04.873 → Aligned 2024-01-01T00:00:00
    • + *
    • P7D → DAY: Raw 2024-01-28T22:12:04.873 → Aligned 2024-01-28T00:00:00
    • + *
    • P1D → HOUR (synthetic): Raw 2024-02-03T22:12:04.873 → Aligned 2024-02-03T22:00:00 (PREPENDED!)
    • + *
    + *
  4. + *
  5. Timeline Splits: + *
      + *
    • P45D → Raw 2023-12-21T22:12:04.873 → Falls in MONTH interval → Aligned 2023-12-01T00:00:00 → CREATES SPLIT
    • + *
    • P14D → Raw 2024-01-21T22:12:04.873 → Falls in DAY interval → Aligned 2024-01-21T00:00:00 → CREATES SPLIT
    • + *
    • P1D is now a segment gran rule (not processed as split)
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 6 intervals + *

    + *
  1. [-∞, 2023-01-01T00:00:00) - YEAR
  2. + *
  3. [2023-01-01T00:00:00, 2023-12-01T00:00:00) - MONTH
  4. + *
  5. [2023-12-01T00:00:00, 2024-01-01T00:00:00) - MONTH
  6. + *
  7. [2024-01-01T00:00:00, 2024-01-21T00:00:00) - DAY
  8. + *
  9. [2024-01-21T00:00:00, 2024-01-28T00:00:00) - DAY
  10. + *
  11. [2024-01-28T00:00:00, 2024-02-03T22:00:00) - HOUR (prepended, note non-midnight end)
  12. + *
+ */ + @Test + public void test_generateAlignedSearchIntervals() + { + DateTime referenceTime = DateTimes.of("2024-02-04T22:12:04.873Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("month-rule", null, Period.years(1), Granularities.YEAR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.MONTH), + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(7), Granularities.DAY) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-7d", null, Period.days(1), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-21d", null, Period.days(45), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.HOUR + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + Assert.assertEquals(6, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(0).getEnd()); + + Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(1).getEnd()); + + Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(2).getStart()); + Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(2).getEnd()); + + Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(3).getStart()); + Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(3).getEnd()); + + Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(4).getStart()); + Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00Z"), intervals.get(4).getEnd()); + + Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00"), intervals.get(5).getStart()); + Assert.assertEquals(DateTimes.of("2024-02-03T22:00:00"), intervals.get(5).getEnd()); + } + + /** + * TEST: No rules at all - should throw IAE + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: None
  • + *
  • Other Rules: None
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * EXPECTED: IllegalArgumentException with message "requires at least one reindexing rule" + */ + @Test + public void test_generateAlignedSearchIntervals_noRulesThrowsException() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> template.generateAlignedSearchIntervals(referenceTime) + ); + + Assert.assertTrue( + exception.getMessage().contains("requires at least one reindexing rule") + ); + } + + /** + * TEST: Split point aligns exactly to existing boundary (boundary snapping, no split created) + *

+ * REFERENCE TIME: 2025-02-01T00:00:00Z (carefully chosen for alignment) + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1M→MONTH
  • + *
  • Other Rules: P1M-metrics (same period as segment gran rule!)
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: None
  2. + *
  3. Initial Timeline: [-∞, 2025-01-01T00:00:00) - MONTH
  4. + *
  5. Timeline Splits: + *
      + *
    • P1M metrics → Raw 2025-01-01T00:00:00 → Aligned to MONTH: 2025-01-01T00:00:00
    • + *
    • This aligns EXACTLY to the existing boundary → no split created
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 1 interval (no split despite having a non-segment-gran rule) + *

    + *
  1. [-∞, 2025-01-01T00:00:00) - MONTH
  2. + *
+ */ + @Test + public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundary() + { + DateTime referenceTime = DateTimes.of("2025-02-01T00:00:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-1m", null, Period.months(1), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + Assert.assertEquals(1, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2025-01-01T00:00:00Z"), intervals.get(0).getEnd()); + } + + /** + * TEST: Prepending that aligns back to last segment gran rule interval end (no prepend actually created) + *

+ * REFERENCE TIME: 2025-01-01T01:00:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1D→DAY
  • + *
  • Other Rules: PT12H-metrics (finer than P1D, but aligns back to same interval end as the P1D rule so no prepend is done)
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Initial Timeline: [-∞, 2024-12-31T00:00:00) - DAY
  2. + *
  3. Check for prepending: + *
      + *
    • PT12H threshold: 2024-12-31T13:00:00
    • + *
    • Align to DAY (default gran): 2024-12-31T00:00:00
    • + *
    • This EQUALS the most recent segment gran rule end (2024-12-31T00:00:00) → NO PREPEND
    • + *
    + *
  4. + *
+ *

+ * EXPECTED OUTPUT: 1 interval (no split prepend despite having a finer non-segment-gran rule) + *

    + *
  1. [-∞, 2024-12-31T00:00:00) - DAY
  2. + *
+ */ + @Test + public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTimeline() + { + DateTime referenceTime = DateTimes.of("2025-01-01T01:00:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(1), Granularities.DAY) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-12h", null, Period.hours(12), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + Assert.assertEquals(1, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-31T00:00:00Z"), intervals.get(0).getEnd()); + } + + /** + * TEST: Multiple split points align to same timestamp (distinct() filtering removes duplicates) + *

+ * REFERENCE TIME: 2025-01-15T00:00:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1M→DAY
  • + *
  • Other Rules: P23D+6h-metrics, P23D+18h-metrics (both align to same DAY boundary in DAY interval)
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: None
  2. + *
  3. Initial Timeline: [-∞, 2024-12-15T00:00:00) - DAY
  4. + *
  5. Timeline Splits: + *
      + *
    • P33D+6h → Raw: 2024-12-12T18:00:00 → Falls in DAY interval → Align to DAY: 2024-12-12T00:00:00
    • + *
    • P33D+18h → Raw: 2024-12-12T06:00:00 → Falls in DAY interval → Align to DAY: 2024-12-12T00:00:00
    • + *
    • Both create split point 2024-12-12T00:00:00 → distinct() removes duplicate!
    • + *
    • Only ONE split created at 2024-12-12T00:00:00
    • + *
    + *
  6. + *
+ *

+ * EXPECTED OUTPUT: 2 intervals (not 3, because duplicate split point was filtered) + *

    + *
  1. [-∞, 2024-12-12T00:00:00) - DAY
  2. + *
  3. [2024-12-12T00:00:00, 2024-12-15T00:00:00) - DAY
  4. + *
+ */ + @Test + public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() + { + DateTime referenceTime = DateTimes.of("2025-01-15T00:00:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.DAY) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-33d-6h", null, Period.hours(33 * 24 + 6), new AggregatorFactory[0]), + new ReindexingMetricsRule("metrics-33d-18h", null, Period.hours(33 * 24 + 18), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + Assert.assertEquals(2, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(0).getEnd()); + + Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(1).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getEnd()); + } + + /** + * TEST: Single rule only (minimal valid case) + *

+ * REFERENCE TIME: 2025-01-29T16:15:00Z + *

+ * INPUT RULES: + *

    + *
  • Segment Granularity Rules: P1M→MONTH
  • + *
  • Other Rules: None
  • + *
  • Default Segment Granularity: DAY
  • + *
+ *

+ * PROCESSING: + *

    + *
  1. Synthetic Rules: None
  2. + *
  3. Initial Timeline: [-∞, 2024-12-01T00:00:00) - MONTH
  4. + *
  5. Timeline Splits: None (no non-segment-gran rules)
  6. + *
+ *

+ * EXPECTED OUTPUT: 1 interval + *

    + *
  1. [-∞, 2024-12-01T00:00:00) - MONTH
  2. + *
+ */ + @Test + public void test_generateAlignedSearchIntervals_singleRuleOnly() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List intervals = template.generateAlignedSearchIntervals(referenceTime); + + Assert.assertEquals(1, intervals.size()); + + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); + Assert.assertEquals(DateTimes.of("2024-12-01T00:00:00Z"), intervals.get(0).getEnd()); + } + private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate { // Capture intervals that were processed for assertions From ce3e1f611ac3ca0e62d82eab60efe8943c666713 Mon Sep 17 00:00:00 2001 From: capistrant Date: Wed, 4 Feb 2026 17:54:20 -0600 Subject: [PATCH 47/90] minor --- .../compact/CascadingReindexingTemplate.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 60e50fba3a23..3ab0c3ce5571 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -447,9 +447,9 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() *
  • Collect olderThan application thresholds from all non-segment-granularity rules
  • *
  • For each interval in the base timeline: *
      - *
    • find thresholds for non-segment granularity rules that fall within it
    • - *
    • Align those thresholds to the interval's segment granularity
    • - *
    • Split base intervals at aligned thresholds
    • + *
    • find olderThan thresholds for non-segment granularity rules that fall within it
    • + *
    • Align those thresholds to the interval's targeted segment granularity using bucketStart on the threshold date
    • + *
    • Split base intervals at the granularity aligned thresholds that were found inside of them
    • *
    *
  • Return the timeline of non-overlapping intervals split for most precise possible rule application (due to segment gran alignment, sometimes rules will be applied later than their explicitly defined period)
  • * @@ -516,7 +516,7 @@ List generateAlignedSearchIntervals(DateTime referenceTime) *
  • If no segment granularity rules exist: *
      *
    1. Find the most recent threshold from non-segment-granularity rules
    2. - *
    3. Use the default granularity to align an interval from [-inf, most recent threshold)
    4. + *
    5. Use the default granularity to granularity align an interval from [-inf, most recent threshold)
    6. *
    *
  • *
  • If segment granularity rules exist: @@ -525,7 +525,7 @@ List generateAlignedSearchIntervals(DateTime referenceTime) *
  • Create intervals for each rule, adjusting the interval end to be aligned to the rule's segment granularity
  • *
  • If non-segment-granularity thresholds exist that are more recent than the most recent segment granularity rule's end: *
      - *
    1. Prepend an interval from [most recent segment granularity end, most recent non-segment-granularity threshold)
    2. + *
    3. Prepend an interval from [most recent segment granularity rule interval end, most recent non-segment-granularity threshold)
    4. *
    *
  • * @@ -611,7 +611,7 @@ private List generateBaseSegmentGranularityAlignedTimel if (alignedEnd.isBefore(mostRecentSegmentGranEnd) || alignedEnd.isEqual(mostRecentSegmentGranEnd)) { LOG.debug( "Most recent non-segment-gran threshold [%s] aligns to [%s], which is not after " - + "most recent segment gran end [%s]. No prepended interval needed.", + + "most recent segment granularity rule interval end [%s]. No prepended interval needed.", mostRecentNonSegmentGranThreshold, alignedEnd, mostRecentSegmentGranEnd From d9889082c8c13c0143fbb9feb4e5529981bd0034 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 6 Feb 2026 15:20:20 -0600 Subject: [PATCH 48/90] Replace Selector with Equals and In with TypedIn --- .../compact/CompactionSupervisorTest.java | 8 ++++---- .../compaction/InlineReindexingRuleProvider.java | 16 ++++++++++------ .../compaction/ReindexingDeletionRuleTest.java | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 4a9cc45d6dea..fe7c09ecf54f 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -43,8 +43,8 @@ import org.apache.druid.java.util.common.jackson.JacksonUtils; import org.apache.druid.query.DruidMetrics; import org.apache.druid.query.expression.TestExprMacroTable; +import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.query.http.ClientSqlQuery; import org.apache.druid.rpc.UpdateResponse; import org.apache.druid.segment.VirtualColumns; @@ -311,7 +311,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac "deletionRule", "Drop rows where item is 'hat'", Period.days(7), - new SelectorDimFilter("item", "hat", null), + new EqualityFilter("item", ColumnType.STRING, "hat", null), null ); @@ -392,7 +392,7 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec "deleteByNestedField", "Remove rows where extraInfo.fieldA = 'valueA'", Period.days(7), - new SelectorDimFilter("extractedFieldA", "valueA", null), + new EqualityFilter("extractedFieldA", ColumnType.STRING, "valueA", null), virtualColumns ); @@ -468,7 +468,7 @@ public void test_compactionWithTransformFilteringAllRows_createsTombstones( .withTransformSpec( // This filter drops all rows: expression "false" always evaluates to false new CompactionTransformSpec( - new NotDimFilter(new SelectorDimFilter("item", "shirt", null)), + new NotDimFilter(new EqualityFilter("item", ColumnType.STRING, "shirt", null)), null ) ); diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index f6a76c3f2623..9cc3cb329a50 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -56,9 +56,10 @@ * "deleteWhere": { * "type": "not", * "field": { - * "type": "selector", - * "dimension": "is_bot", - * "value": "true" + * "type": "equals", + * "column": "is_bot", + * "matchValueType": "STRING" + * "matchValue": "true" * } * }, * "description": "Remove bot traffic from segments older than 90 days" @@ -69,9 +70,12 @@ * "deleteWhere": { * "type": "not", * "field": { - * "type": "in", - * "dimension": "priority", - * "values": ["low", "spam"] + * { + * "type": "inType", + * "column": "priority", + * "matchValueType": "STRING", + * "sortedValues": ["low", "spam"] + * } * } * }, * "description": "Remove low-priority data from segments older than 180 days" diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java index 6af4b8f07e16..cc93ec66cce0 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java @@ -24,7 +24,7 @@ import org.apache.druid.java.util.common.Intervals; import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; @@ -39,7 +39,7 @@ public class ReindexingDeletionRuleTest private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); private static final Period PERIOD_30_DAYS = Period.days(30); - private final DimFilter testFilter = new SelectorDimFilter("isRobot", "true", null); + private final DimFilter testFilter = new EqualityFilter("isRobot", ColumnType.STRING, "true", null); private final VirtualColumns virtualColumns = VirtualColumns.create( ImmutableList.of( new ExpressionVirtualColumn( From 3f75762100aa9ae70f3319913c5583f7c0a3132b Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 6 Feb 2026 16:37:50 -0600 Subject: [PATCH 49/90] Add strict enforcement of segment granularity changes --- .../compact/CascadingReindexingTemplate.java | 53 ++++++++ .../CascadingReindexingTemplateTest.java | 121 ++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 3ab0c3ce5571..4af15f43228c 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -635,9 +635,62 @@ private List generateBaseSegmentGranularityAlignedTimel } } + // Validate the completed timeline before returning + validateSegmentGranularityTimeline(baseTimeline); + return baseTimeline; } + /** + * Validates that the completed segment granularity timeline follows the constraint that + * granularity must stay the same or become finer as we move from past to present. + *

    + * This ensures that operators cannot misconfigure rules that would cause data to be + * recompacted from coarse to fine granularity as it ages (e.g., DAY -> HOUR), + * which is typically undesirable and inefficient. + * + * @param timeline the completed base timeline with granularity information + * @throws IAE if granularity becomes coarser as we move toward present + */ + private void validateSegmentGranularityTimeline(List timeline) + { + if (timeline.size() <= 1) { + return; // Nothing to validate + } + + for (int i = 1; i < timeline.size(); i++) { + IntervalWithGranularity olderInterval = timeline.get(i - 1); + IntervalWithGranularity newerInterval = timeline.get(i); + + Granularity olderGran = olderInterval.granularity; + Granularity newerGran = newerInterval.granularity; + + // As we move from past (older intervals) to present (newer intervals), + // granularity should stay the same or get finer. + // If the older interval's granularity is finer than the newer interval's granularity, + // that means we're getting coarser as we move toward present, which is invalid. + if (olderGran.isFinerThan(newerGran)) { + throw new IAE( + "Invalid segment granularity timeline for dataSource[%s]: " + + "Interval[%s] with granularity[%s] is more recent than " + + "interval[%s] with granularity[%s], but has a coarser granularity. " + + "Segment granularity must stay the same or become finer as data ages from present to past.", + dataSource, + newerInterval.interval, + newerGran, + olderInterval.interval, + olderGran + ); + } + } + + LOG.debug( + "Segment granularity timeline validation passed for dataSource[%s] with [%d] intervals", + dataSource, + timeline.size() + ); + } + /** * Collects thresholds from all non-segment-granularity rules. */ diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 022a2aea082e..eef256daa6c0 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -1197,6 +1197,127 @@ private CompactionJobParams createMockParams(DateTime referenceTime, SegmentTime return mockParams; } + /** + * TEST: Validation failure - default granularity is coarser than most recent segment granularity rule + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P30D→HOUR, P90D→DAY
    • + *
    • Other Rules: P7D-metrics (finer than P30D, triggers prepending with default granularity)
    • + *
    • Default Segment Granularity: MONTH (COARSER than HOUR!)
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Sort rules by period: P90D→DAY (oldest), P30D→HOUR (newest)
    2. + *
    3. P7D metrics is finer than P30D, so prepend interval with default MONTH granularity
    4. + *
    5. Timeline would be: [-∞, DAY_boundary) DAY, [DAY_boundary, HOUR_boundary) HOUR, [HOUR_boundary, MONTH_boundary) MONTH
    6. + *
    7. Validation: HOUR → MONTH progression means granularity is getting COARSER toward present
    8. + *
    + *

    + * EXPECTED: IllegalArgumentException with message about invalid granularity timeline + */ + @Test + public void test_generateAlignedSearchIntervals_failsWhenDefaultGranularityIsCoarserThanMostRecentSegmentGranRule() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) + )) + .metricsRules(List.of( + new ReindexingMetricsRule("metrics-7d", null, Period.days(7), new AggregatorFactory[0]) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.MONTH // MONTH is coarser than HOUR! + ); + + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> template.generateAlignedSearchIntervals(referenceTime) + ); + + Assert.assertTrue( + exception.getMessage().contains("Invalid segment granularity timeline") + ); + Assert.assertTrue( + exception.getMessage().contains("coarser granularity") + ); + } + + /** + * TEST: Validation failure - older rule has finer granularity than newer rule + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P30D→DAY, P90D→HOUR
    • + *
    • Other Rules: None
    • + *
    • Default Segment Granularity: DAY
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Sort rules by period: P90D→HOUR (oldest), P30D→DAY (newest)
    2. + *
    3. Timeline would be: [-∞, HOUR_boundary) HOUR, [HOUR_boundary, DAY_boundary) DAY
    4. + *
    5. Validation: HOUR → DAY progression means granularity is getting COARSER toward present
    6. + *
    7. This violates the constraint: older data (P90D) has HOUR granularity, newer data (P30D) has DAY granularity
    8. + *
    + *

    + * EXPECTED: IllegalArgumentException with message about invalid granularity timeline + */ + @Test + public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranularityThanNewerRule() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(30), Granularities.DAY), + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(90), Granularities.HOUR) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> template.generateAlignedSearchIntervals(referenceTime) + ); + + Assert.assertTrue( + exception.getMessage().contains("Invalid segment granularity timeline") + ); + Assert.assertTrue( + exception.getMessage().contains("coarser granularity") + ); + } + private DruidInputSource createMockSource() { final Interval[] capturedInterval = new Interval[1]; From 2293e86ede733c86199c68a7ab44105cfacff21e Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 9 Feb 2026 11:15:20 -0600 Subject: [PATCH 50/90] Fix bug introduced that was breaking MVD in compaction --- .../java/org/apache/druid/msq/indexing/MSQCompactionRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java index d429a6f97dc8..e65a1994f4aa 100644 --- a/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java +++ b/multi-stage-query/src/main/java/org/apache/druid/msq/indexing/MSQCompactionRunner.java @@ -653,7 +653,7 @@ private Query buildGroupByQuery( List postAggregators = inputColToVirtualCol.entrySet() .stream() - .filter(entry -> entry.getKey().startsWith(ARRAY_VIRTUAL_COLUMN_PREFIX)) + .filter(entry -> entry.getValue().getOutputName().startsWith(ARRAY_VIRTUAL_COLUMN_PREFIX)) .map( entry -> new ExpressionPostAggregator( From 7030054883420e1d379975932c242d319d5181e8 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 13 Feb 2026 12:35:10 -0600 Subject: [PATCH 51/90] Add ReindexingDataSchema rule that will replace other rule types --- .../compact/CascadingReindexingTemplate.java | 7 +- .../ComposingReindexingRuleProvider.java | 46 +++---- .../InlineReindexingRuleProvider.java | 36 +++++- .../compaction/ReindexingConfigBuilder.java | 91 ++++++-------- .../compaction/ReindexingDataSchemaRule.java | 89 +++++++++++++ .../compaction/ReindexingRuleProvider.java | 23 +++- ...nlineSchemaDataSourceCompactionConfig.java | 28 +++++ .../ComposingReindexingRuleProviderTest.java | 48 ++++++- .../InlineReindexingRuleProviderTest.java | 44 ++++++- .../ReindexingConfigBuilderTest.java | 119 +++++++++++------- 10 files changed, 402 insertions(+), 129 deletions(-) create mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 4af15f43228c..fafcfeae7f8b 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -330,7 +330,12 @@ public List createCompactionJobs( InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); - ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder(ruleProvider, reindexingInterval, currentTime); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( + ruleProvider, + defaultSegmentGranularity, + reindexingInterval, + currentTime + ); int ruleCount = configBuilder.applyTo(builder); if (ruleCount > 0) { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index a893e6e8259d..3fa0796fd60b 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -58,35 +58,14 @@ * "segmentGranularity": "HOUR" * } * ] - * }, - * { - * "type": "inline", - * "segmentGranularityRules": [ - * { - * "id": "default-granularity", - * "olderThan": "P1D", - * "segmentGranularity": "DAY" - * } - * ], - * "deletionRules": [ - * { - * "id": "remove-bots", - * "olderThan": "P30D", - * "deleteWhere": { - * "type": "selector", - * "dimension": "isRobot", - * "value": "true" - * } - * } - * ] * } * ] * } * }

    * In this example: *
      - *
    • Granularity rules come from the first provider (HOUR segment granularity for recent data)
    • - *
    • Deletion rules come from the second provider (first provider with rules)
    • + *
    • Composing rule with a single provider to simply show the inline definition.
    • + *
    • Once multiple provider types exist, this will allow operators to chain them as needed.
    • *
    */ public class ComposingReindexingRuleProvider implements ReindexingRuleProvider @@ -156,6 +135,27 @@ public List getMetricsRules() .orElse(Collections.emptyList()); } + @Override + @Nullable + public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime) + { + return providers.stream() + .map(p -> p.getDataSchemaRule(interval, referenceTime)) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + } + + @Override + public List getDataSchemaRules() + { + return providers.stream() + .map(ReindexingRuleProvider::getDataSchemaRules) + .filter(rules -> !rules.isEmpty()) + .findFirst() + .orElse(Collections.emptyList()); + } + @Override @Nullable public ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime) diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index 9cc3cb329a50..f2996c6d27db 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -96,6 +96,7 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider private final List reindexingSegmentGranularityRules; private final List reindexingQueryGranularityRules; private final List reindexingTuningConfigRules; + private final List reindexingDataSchemaRules; @JsonCreator @@ -107,7 +108,8 @@ public InlineReindexingRuleProvider( @JsonProperty("reindexingProjectionRules") @Nullable List reindexingProjectionRules, @JsonProperty("reindexingSegmentGranularityRules") @Nullable List reindexingSegmentGranularityRules, @JsonProperty("reindexingQueryGranularityRules") @Nullable List reindexingQueryGranularityRules, - @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules + @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules, + @JsonProperty("reindexingDataSchemaRules") @Nullable List reindexingDataSchemaRules ) { this.reindexingDeletionRules = Configs.valueOrDefault(reindexingDeletionRules, Collections.emptyList()); @@ -118,6 +120,7 @@ public InlineReindexingRuleProvider( this.reindexingSegmentGranularityRules = Configs.valueOrDefault(reindexingSegmentGranularityRules, Collections.emptyList()); this.reindexingQueryGranularityRules = Configs.valueOrDefault(reindexingQueryGranularityRules, Collections.emptyList()); this.reindexingTuningConfigRules = Configs.valueOrDefault(reindexingTuningConfigRules, Collections.emptyList()); + this.reindexingDataSchemaRules = Configs.valueOrDefault(reindexingDataSchemaRules, Collections.emptyList()); } public static Builder builder() @@ -146,6 +149,20 @@ public List getMetricsRules() return reindexingMetricsRules; } + @Override + @Nullable + public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime) + { + return getApplicableRule(reindexingDataSchemaRules, interval, referenceTime); + } + + @Override + @JsonProperty("reindexingDataSchemaRules") + public List getDataSchemaRules() + { + return reindexingDataSchemaRules; + } + @Override @JsonProperty("reindexingDimensionsRules") public List getDimensionsRules() @@ -312,7 +329,8 @@ public boolean equals(Object o) && Objects.equals(reindexingProjectionRules, that.reindexingProjectionRules) && Objects.equals(reindexingSegmentGranularityRules, that.reindexingSegmentGranularityRules) && Objects.equals(reindexingQueryGranularityRules, that.reindexingQueryGranularityRules) - && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules); + && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules) + && Objects.equals(reindexingDataSchemaRules, that.reindexingDataSchemaRules); } @Override @@ -326,7 +344,8 @@ public int hashCode() reindexingProjectionRules, reindexingSegmentGranularityRules, reindexingQueryGranularityRules, - reindexingTuningConfigRules + reindexingTuningConfigRules, + reindexingDataSchemaRules ); } @@ -342,6 +361,7 @@ public String toString() + ", reindexingSegmentGranularityRules=" + reindexingSegmentGranularityRules + ", reindexingQueryGranularityRules=" + reindexingQueryGranularityRules + ", reindexingTuningConfigRules=" + reindexingTuningConfigRules + + ", reindexingDataSchemaRules=" + reindexingDataSchemaRules + '}'; } @@ -355,6 +375,7 @@ public static class Builder private List reindexingSegmentGranularityRules; private List reindexingQueryGranularityRules; private List reindexingTuningConfigRules; + private List reindexingDataSchemaRules; public Builder deletionRules(List reindexingDeletionRules) { @@ -362,6 +383,12 @@ public Builder deletionRules(List reindexingDeletionRule return this; } + public Builder dataSchemaRules(List reindexingDataSchemaRules) + { + this.reindexingDataSchemaRules = reindexingDataSchemaRules; + return this; + } + public Builder metricsRules(List reindexingMetricsRules) { this.reindexingMetricsRules = reindexingMetricsRules; @@ -414,7 +441,8 @@ public InlineReindexingRuleProvider build() reindexingProjectionRules, reindexingSegmentGranularityRules, reindexingQueryGranularityRules, - reindexingTuningConfigRules + reindexingTuningConfigRules, + reindexingDataSchemaRules ); } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index d0dbccdc09d0..abebf0dc5c58 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -48,16 +48,18 @@ public class ReindexingConfigBuilder private static final Logger LOG = new Logger(ReindexingConfigBuilder.class); private final ReindexingRuleProvider provider; + private final Granularity defaultGranularity; private final Interval interval; private final DateTime referenceTime; public ReindexingConfigBuilder( ReindexingRuleProvider provider, - Interval interval, + Granularity defaultSegmentGranularity, Interval interval, DateTime referenceTime ) { this.provider = provider; + this.defaultGranularity = defaultSegmentGranularity; this.interval = interval; this.referenceTime = referenceTime; } @@ -77,33 +79,26 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) ReindexingTuningConfigRule::getTuningConfig ); - count += applyIfPresent( - builder::withMetricsSpec, - provider.getMetricsRule(interval, referenceTime), - ReindexingMetricsRule::getMetricsSpec - ); - - count += applyIfPresent( - builder::withDimensionsSpec, - provider.getDimensionsRule(interval, referenceTime), - ReindexingDimensionsRule::getDimensionsSpec - ); - count += applyIfPresent( builder::withIoConfig, provider.getIOConfigRule(interval, referenceTime), ReindexingIOConfigRule::getIoConfig ); - count += applyIfPresent( - builder::withProjections, - provider.getProjectionRule(interval, referenceTime), - ReindexingProjectionRule::getProjections - ); + count += applyDataSchemaRules(builder); - count += applyGranularityRules(builder); + count += applyDeletionRules(builder); - count += applyFilterRules(builder); + ReindexingSegmentGranularityRule segmentGranularityRule = provider.getSegmentGranularityRule(interval, referenceTime); + if (segmentGranularityRule == null) { + if (count > 0) { + // Insert a default segment granularity to the config only if other rules exist for the interval. + builder.withSegmentGranularity(defaultGranularity); + } + } else { + count++; + builder.withSegmentGranularity(segmentGranularityRule.getSegmentGranularity()); + } return count; } @@ -118,56 +113,50 @@ private int applyIfPresent( if (rule != null) { C config = configExtractor.apply(rule); setter.accept(config); - LOG.debug( - "Applied rule %s for interval %s", - ((ReindexingRule) rule).getId(), interval - ); return 1; } return 0; } - private int applyGranularityRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + private int applyDataSchemaRules(InlineSchemaDataSourceCompactionConfig.Builder builder) { - ReindexingSegmentGranularityRule segmentGranularityRule = provider.getSegmentGranularityRule( - interval, - referenceTime - ); - ReindexingQueryGranularityRule queryGranularityRule = provider.getQueryGranularityRule( + ReindexingDataSchemaRule dataSchemaRule = provider.getDataSchemaRule( interval, referenceTime ); - - if (segmentGranularityRule == null && queryGranularityRule == null) { + if (dataSchemaRule == null) { return 0; } - // Extract granularities from rules (null if rule doesn't exist) - Granularity segmentGranularity = segmentGranularityRule != null ? segmentGranularityRule.getSegmentGranularity() : null; - - Granularity queryGranularity = queryGranularityRule != null ? queryGranularityRule.getQueryGranularity() : null; - Boolean rollup = queryGranularityRule != null ? queryGranularityRule.getRollup() : null; + applyIfPresent( + builder::withDimensionsSpec, + dataSchemaRule, + ReindexingDataSchemaRule::getDimensionsSpec + ); - // Build and apply the combined granularity config - UserCompactionTaskGranularityConfig granularityConfig = - new UserCompactionTaskGranularityConfig(segmentGranularity, queryGranularity, rollup); + applyIfPresent( + builder::withMetricsSpec, + dataSchemaRule, + ReindexingDataSchemaRule::getMetricsSpec + ); - builder.withGranularitySpec(granularityConfig); + applyIfPresent( + builder::withProjections, + dataSchemaRule, + ReindexingDataSchemaRule::getProjections + ); - int count = 0; - if (segmentGranularityRule != null) { - LOG.debug("Applied segment granularity rule [%s] for interval [%s]", segmentGranularityRule.getId(), interval); - count++; - } - if (queryGranularityRule != null) { - LOG.debug("Applied query granularity rule [%s] for interval [%s]", queryGranularityRule.getId(), interval); - count++; + if (dataSchemaRule.getQueryGranularity() != null || dataSchemaRule.getRollup() != null) { + builder.withQueryGranularityAndRollup( + dataSchemaRule.getQueryGranularity(), + dataSchemaRule.getRollup() + ); } - return count; + return 1; } - private int applyFilterRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + private int applyDeletionRules(InlineSchemaDataSourceCompactionConfig.Builder builder) { List rules = provider.getDeletionRules(interval, referenceTime); if (rules.isEmpty()) { diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java new file mode 100644 index 000000000000..bbf80cc60070 --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java @@ -0,0 +1,89 @@ +/* + * 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.compaction; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.joda.time.Period; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public class ReindexingDataSchemaRule extends AbstractReindexingRule +{ + private final UserCompactionTaskDimensionsConfig dimensionsSpec; + private final AggregatorFactory[] metricsSpec; + private final Granularity queryGranularity; + private final Boolean rollup; + private final List projections; + + public ReindexingDataSchemaRule( + @JsonProperty("id") @Nonnull String id, + @JsonProperty("description") @Nullable String description, + @JsonProperty("olderThan") @Nonnull Period olderThan, + @JsonProperty("dimensionsSpec") @Nullable UserCompactionTaskDimensionsConfig dimensionsSpec, + @JsonProperty("metricsSpec") @Nullable AggregatorFactory[] metricsSpec, + @JsonProperty("queryGranularity") @Nullable Granularity queryGranularity, + @JsonProperty("rolloup") @Nullable Boolean rollup, + @JsonProperty("projections") @Nullable List projections + ) + { + super(id, description, olderThan); + this.dimensionsSpec = dimensionsSpec; + this.metricsSpec = metricsSpec; + this.queryGranularity = queryGranularity; + this.rollup = rollup; + this.projections = projections; + } + + @JsonProperty + public UserCompactionTaskDimensionsConfig getDimensionsSpec() + { + return dimensionsSpec; + } + + @JsonProperty + public List getProjections() + { + return projections; + } + + @JsonProperty + public AggregatorFactory[] getMetricsSpec() + { + return metricsSpec; + } + + @JsonProperty + public Granularity getQueryGranularity() + { + return queryGranularity; + } + + @JsonProperty + public Boolean getRollup() + { + return rollup; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index 4d5649af431a..b259e8d14240 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -107,6 +107,26 @@ default boolean isReady() */ List getMetricsRules(); + /** + * Returns the matched reindexing data schema rule that applies to the given interval. + *

    + * Handling cases of multiple applicable rules and/orpartial overlaps is the responsibility of the provider + * implementation and should be clearly documented. + *

    + * + * @param interval The interval to check applicability against. + * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. + * e.g., a rule with period P7D applies to data older than 7 days from the reference time. + * @return {@link ReindexingDataSchemaRule} rule that applies to the given interval. + */ + @Nullable + ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime); + + /** + * Returns ALL reindexing data schema rules. + */ + List getDataSchemaRules(); + /** * Returns the matched reindexing dimensions rule that applies to the given interval. *

    @@ -245,7 +265,8 @@ default Stream streamAllRules() getQueryGranularityRules().stream(), getTuningConfigRules().stream(), getDeletionRules().stream(), - getSegmentGranularityRules().stream() + getSegmentGranularityRules().stream(), + getDataSchemaRules().stream() ).flatMap(s -> s); } } diff --git a/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java b/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java index d75d22dda5c5..58a7206b8ab8 100644 --- a/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java +++ b/server/src/main/java/org/apache/druid/server/coordinator/InlineSchemaDataSourceCompactionConfig.java @@ -383,6 +383,34 @@ public Builder withGranularitySpec( return this; } + public Builder withSegmentGranularity(Granularity segmentGranularity) + { + if (this.granularitySpec == null) { + this.granularitySpec = new UserCompactionTaskGranularityConfig(segmentGranularity, null, null); + } else { + this.granularitySpec = new UserCompactionTaskGranularityConfig( + segmentGranularity, + this.granularitySpec.getQueryGranularity(), + this.granularitySpec.isRollup() + ); + } + return this; + } + + public Builder withQueryGranularityAndRollup(Granularity queryGranularity, Boolean rollup) + { + if (this.granularitySpec == null) { + this.granularitySpec = new UserCompactionTaskGranularityConfig(null, queryGranularity, rollup); + } else { + this.granularitySpec = new UserCompactionTaskGranularityConfig( + this.granularitySpec.getSegmentGranularity(), + queryGranularity, + rollup + ); + } + return this; + } + public Builder withDimensionsSpec( UserCompactionTaskDimensionsConfig dimensionsSpec ) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 96511cfa480a..75167b27f680 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -143,6 +143,30 @@ public void test_getDeletionRulesWithInterval_compositingBehavior() ); } + @Test + public void test_getDataSchemaRules_compositingBehavior() + { + testComposingBehaviorForRuleType( + rules -> InlineReindexingRuleProvider.builder().dataSchemaRules(rules).build(), + ComposingReindexingRuleProvider::getDataSchemaRules, + createDataSchemaRule("rule1", Period.days(7)), + createDataSchemaRule("rule2", Period.days(30)), + ReindexingDataSchemaRule::getId + ); + } + + @Test + public void test_getDataSchemaRulesWithInterval_compositingBehavior() + { + testComposingBehaviorForNonAdditiveRuleTypeWithInterval( + rules -> InlineReindexingRuleProvider.builder().dataSchemaRules(rules).build(), + (provider, it) -> provider.getDataSchemaRule(it.interval, it.time), + createDataSchemaRule("rule1", Period.days(7)), + createDataSchemaRule("rule2", Period.days(30)), + ReindexingDataSchemaRule::getId + ); + } + @Test public void test_getSegmentGranularityRules_compositingBehavior() { @@ -497,7 +521,7 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( */ private ReindexingRuleProvider createNotReadyProvider() { - return new InlineReindexingRuleProvider(null, null, null, null, null, null, null, null) + return new InlineReindexingRuleProvider(null, null, null, null, null, null, null, null, null) { @Override public boolean isReady() @@ -598,4 +622,26 @@ private ReindexingTuningConfigRule createTuningConfigRule(String id, Period peri ); } + private ReindexingDataSchemaRule createDataSchemaRule(String id, Period period) + { + return new ReindexingDataSchemaRule( + id, + "Test data schema rule", + period, + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.DAY, + true, + ImmutableList.of( + new AggregateProjectionSpec( + "test_projection", + null, + null, + null, + new AggregatorFactory[]{new CountAggregatorFactory("count")} + ) + ) + ); + } + } diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 9a3ab21023d3..ba27044e9571 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -58,8 +58,16 @@ public class InlineReindexingRuleProviderTest @Test public void test_constructor_nullListsDefaultToEmpty() { - InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider(null, null, null, null, - null, null, null, null + InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider( + null, + null, + null, + null, + null, + null, + null, + null, + null ); Assert.assertNotNull(provider.getDeletionRules()); @@ -78,6 +86,7 @@ public void test_constructor_nullListsDefaultToEmpty() Assert.assertTrue(provider.getQueryGranularityRules().isEmpty()); Assert.assertNotNull(provider.getTuningConfigRules()); Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); + Assert.assertTrue(provider.getDataSchemaRules().isEmpty()); } @Test @@ -163,6 +172,13 @@ public void test_allNonAdditiveRules_validateNonAdditivity() InlineReindexingRuleProvider.Builder::tuningConfigRules, InlineReindexingRuleProvider::getTuningConfigRule ); + + testNonAdditivity( + "dataSchema", + this::createDataSchemaRule, + InlineReindexingRuleProvider.Builder::dataSchemaRules, + InlineReindexingRuleProvider::getDataSchemaRule + ); } @Test @@ -176,6 +192,7 @@ public void test_allRuleTypesWireCorrectly_withInterval() ReindexingSegmentGranularityRule segmentGranularityRule = createSegmentGranularityRule("segmentGranularity", Period.days(30)); ReindexingQueryGranularityRule queryGranularityRule = createQueryGranularityRule("queryGranularity", Period.days(30)); ReindexingTuningConfigRule tuningConfigRule = createTuningConfigRule("tuning", Period.days(30)); + ReindexingDataSchemaRule dataSchemaRule = createDataSchemaRule("dataSchema", Period.days(30)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .deletionRules(ImmutableList.of(filterRule)) @@ -186,6 +203,7 @@ public void test_allRuleTypesWireCorrectly_withInterval() .segmentGranularityRules(ImmutableList.of(segmentGranularityRule)) .queryGranularityRules(ImmutableList.of(queryGranularityRule)) .tuningConfigRules(ImmutableList.of(tuningConfigRule)) + .dataSchemaRules(ImmutableList.of(dataSchemaRule)) .build(); Assert.assertEquals(1, provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); @@ -204,6 +222,8 @@ public void test_allRuleTypesWireCorrectly_withInterval() Assert.assertEquals("queryGranularity", provider.getQueryGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); Assert.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + + Assert.assertEquals("dataSchema", provider.getDataSchemaRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); } /** @@ -334,4 +354,24 @@ private ReindexingTuningConfigRule createTuningConfigRule(String id, Period peri ) ); } + + private ReindexingDataSchemaRule createDataSchemaRule(String id, Period period) + { + return new ReindexingDataSchemaRule( + id, + null, + period, + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.DAY, + true, + ImmutableList.of(new AggregateProjectionSpec( + "test_projection", + null, + null, + null, + new AggregatorFactory[]{new CountAggregatorFactory("count")} + )) + ); + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 799697fb1c2a..ccc6f3879c10 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -45,6 +45,50 @@ public class ReindexingConfigBuilderTest private static final Interval TEST_INTERVAL = Intervals.of("2024-11-01/2024-11-02"); private static final DateTime REFERENCE_TIME = DateTimes.of("2025-01-15"); + @Test + public void test_applyTo_handlesSynteticSegmentGranularityInsertion() + { + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .dataSchemaRules( + ImmutableList.of( + new ReindexingDataSchemaRule( + "schema-30d", + null, + Period.days(30), + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.HOUR, + true, + ImmutableList.of() + ) + ) + ).build(); + + InlineSchemaDataSourceCompactionConfig.Builder builder = + InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource("test_datasource"); + + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( + provider, + Granularities.DAY, + TEST_INTERVAL, + REFERENCE_TIME + ); + + int count = configBuilder.applyTo(builder); + + Assert.assertEquals(1, count); + + InlineSchemaDataSourceCompactionConfig config = builder.build(); + Assert.assertNotNull(config.getGranularitySpec()); + Assert.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); + Assert.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); + Assert.assertNotNull(config.getGranularitySpec().getQueryGranularity()); + Assert.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); + Assert.assertNotNull(config.getGranularitySpec().isRollup()); + Assert.assertTrue(config.getGranularitySpec().isRollup()); + } + @Test public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount() { @@ -55,13 +99,14 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, + Granularities.DAY, TEST_INTERVAL, REFERENCE_TIME ); int count = configBuilder.applyTo(builder); - Assert.assertEquals(9, count); // 7 non-additive + 2 filter rules + Assert.assertEquals(6, count); InlineSchemaDataSourceCompactionConfig config = builder.build(); @@ -81,7 +126,7 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount Assert.assertNotNull(config.getIoConfig()); Assert.assertNotNull(config.getProjections()); - Assert.assertEquals(1, config.getProjections().size()); // 1 from rule2 + Assert.assertEquals(1, config.getProjections().size()); // only 1 as we match the 2nd dataSchemaRule Assert.assertNotNull(config.getTransformSpec()); DimFilter appliedFilter = config.getTransformSpec().getFilter(); @@ -104,6 +149,7 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, + Granularities.DAY, TEST_INTERVAL, REFERENCE_TIME ); @@ -132,14 +178,6 @@ private ReindexingRuleProvider createFullyPopulatedProvider() Granularities.DAY ); - ReindexingQueryGranularityRule queryGranularityRule = new ReindexingQueryGranularityRule( - "query-gran-30d", - null, - Period.days(30), - Granularities.HOUR, - true - ); - ReindexingTuningConfigRule tuningConfigRule = new ReindexingTuningConfigRule( "tuning-30d", null, @@ -149,18 +187,20 @@ private ReindexingRuleProvider createFullyPopulatedProvider() null, null, null, null, null, null, null) ); - ReindexingMetricsRule metricsRule = new ReindexingMetricsRule( - "metrics-30d", + ReindexingDeletionRule filterRule1 = new ReindexingDeletionRule( + "filter-30d", null, Period.days(30), - new AggregatorFactory[]{new CountAggregatorFactory("count")} + new SelectorDimFilter("country", "US", null), + null ); - ReindexingDimensionsRule dimensionsRule = new ReindexingDimensionsRule( - "dims-30d", + ReindexingDeletionRule filterRule2 = new ReindexingDeletionRule( + "filter-60d", null, - Period.days(30), - new UserCompactionTaskDimensionsConfig(null) + Period.days(60), + new SelectorDimFilter("device", "mobile", null), + null ); ReindexingIOConfigRule ioConfigRule = new ReindexingIOConfigRule( @@ -170,55 +210,42 @@ private ReindexingRuleProvider createFullyPopulatedProvider() new UserCompactionTaskIOConfig(null) ); - // Two projection rules (additive) - ReindexingProjectionRule projectionRule1 = new ReindexingProjectionRule( - "proj-30d", + ReindexingDataSchemaRule dataSchemaRule1 = new ReindexingDataSchemaRule( + "schema-30d", null, Period.days(30), + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.HOUR, + true, ImmutableList.of( new AggregateProjectionSpec("proj1", null, null, null, - new AggregatorFactory[]{new CountAggregatorFactory("count1")}), + new AggregatorFactory[]{new CountAggregatorFactory("count1")}), new AggregateProjectionSpec("proj2", null, null, null, - new AggregatorFactory[]{new CountAggregatorFactory("count2")}) + new AggregatorFactory[]{new CountAggregatorFactory("count2")}) ) ); - ReindexingProjectionRule projectionRule2 = new ReindexingProjectionRule( - "proj-60d", + ReindexingDataSchemaRule dataSchemaRule2 = new ReindexingDataSchemaRule( + "schema-60d", null, Period.days(60), + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.HOUR, + true, ImmutableList.of( new AggregateProjectionSpec("proj3", null, null, null, - new AggregatorFactory[]{new CountAggregatorFactory("count3")}) + new AggregatorFactory[]{new CountAggregatorFactory("count3")}) ) ); - // Two filter rules (additive) - ReindexingDeletionRule filterRule1 = new ReindexingDeletionRule( - "filter-30d", - null, - Period.days(30), - new SelectorDimFilter("country", "US", null), - null - ); - - ReindexingDeletionRule filterRule2 = new ReindexingDeletionRule( - "filter-60d", - null, - Period.days(60), - new SelectorDimFilter("device", "mobile", null), - null - ); - return InlineReindexingRuleProvider.builder() .segmentGranularityRules(ImmutableList.of(segmentGranularityRule)) - .queryGranularityRules(ImmutableList.of(queryGranularityRule)) .tuningConfigRules(ImmutableList.of(tuningConfigRule)) - .metricsRules(ImmutableList.of(metricsRule)) - .dimensionsRules(ImmutableList.of(dimensionsRule)) .ioConfigRules(ImmutableList.of(ioConfigRule)) - .projectionRules(ImmutableList.of(projectionRule1, projectionRule2)) .deletionRules(ImmutableList.of(filterRule1, filterRule2)) + .dataSchemaRules(ImmutableList.of(dataSchemaRule1, dataSchemaRule2)) .build(); } } From 6f1bd027a85cc3e6e7ca878219f31a108ef691e0 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 13 Feb 2026 14:44:30 -0600 Subject: [PATCH 52/90] Tear out rules that are now consolidated into data schema rules --- .../CascadingReindexingTemplateTest.java | 70 +++--- .../ComposingReindexingRuleProvider.java | 84 -------- .../InlineReindexingRuleProvider.java | 115 ---------- .../compaction/ReindexingConfigBuilder.java | 1 - .../compaction/ReindexingDimensionsRule.java | 77 ------- .../compaction/ReindexingMetricsRule.java | 76 ------- .../compaction/ReindexingProjectionRule.java | 79 ------- .../ReindexingQueryGranularityRule.java | 81 ------- .../compaction/ReindexingRuleProvider.java | 84 -------- .../ComposingReindexingRuleProviderTest.java | 146 +------------ .../InlineReindexingRuleProviderTest.java | 98 --------- .../ReindexingDimensionsRuleTest.java | 167 --------------- .../compaction/ReindexingMetricsRuleTest.java | 173 --------------- .../ReindexingProjectionRuleTest.java | 163 -------------- .../ReindexingQueryGranularityRuleTest.java | 201 ------------------ 15 files changed, 28 insertions(+), 1587 deletions(-) delete mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java delete mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java delete mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java delete mode 100644 server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java delete mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java delete mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java delete mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java delete mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index eef256daa6c0..402dc86d5fd6 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -30,7 +30,7 @@ import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; -import org.apache.druid.server.compaction.ReindexingMetricsRule; +import org.apache.druid.server.compaction.ReindexingDataSchemaRule; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; @@ -480,23 +480,11 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY), new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH) )) - .metricsRules(List.of( - new ReindexingMetricsRule( - "metrics-8d", null, Period.days(8), - new AggregatorFactory[0] - ), - new ReindexingMetricsRule( - "metrics-14d", null, Period.days(14), - new AggregatorFactory[0] - ), - new ReindexingMetricsRule( - "metrics-45d", null, Period.days(45), - new AggregatorFactory[0] - ), - new ReindexingMetricsRule( - "metrics-100d", null, Period.days(100), - new AggregatorFactory[0] - ) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-8d", null, Period.days(8), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-45d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-100d", null, Period.days(100), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -576,10 +564,10 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-8d", null, Period.days(8), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-45d", null, Period.days(45), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-8d", null, Period.days(8), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-45d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -659,10 +647,10 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH), new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-7d", null, Period.days(7), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-21d", null, Period.days(21), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-7d", null, Period.days(7), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-21d", null, Period.days(21), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -751,10 +739,10 @@ public void test_generateAlignedSearchIntervals() new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.MONTH), new ReindexingSegmentGranularityRule("day-rule", null, Period.days(7), Granularities.DAY) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-7d", null, Period.days(1), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-14d", null, Period.days(14), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-21d", null, Period.days(45), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-7d", null, Period.days(1), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-21d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -874,8 +862,8 @@ public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundar .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-1m", null, Period.months(1), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-1m", null, Period.months(1), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -937,8 +925,8 @@ public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTim .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("day-rule", null, Period.days(1), Granularities.DAY) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-12h", null, Period.hours(12), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-12h", null, Period.hours(12), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -1003,9 +991,9 @@ public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() .segmentGranularityRules(List.of( new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.DAY) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-33d-6h", null, Period.hours(33 * 24 + 6), new AggregatorFactory[0]), - new ReindexingMetricsRule("metrics-33d-18h", null, Period.hours(33 * 24 + 18), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-33d-6h", null, Period.hours(33 * 24 + 6), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-33d-18h", null, Period.hours(33 * 24 + 18), null, new AggregatorFactory[0], null, null, null) )) .build(); @@ -1177,11 +1165,7 @@ private ReindexingRuleProvider createMockProvider(List periods) // Return a fresh stream on each call to avoid "stream has already been operated upon or closed" errors EasyMock.expect(mockProvider.streamAllRules()).andAnswer(() -> segmentGranularityRules.stream().map(r -> (ReindexingRule) r)).anyTimes(); EasyMock.expect(mockProvider.getSegmentGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(segmentGranularityRules.get(0)).anyTimes(); - EasyMock.expect(mockProvider.getQueryGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); - EasyMock.expect(mockProvider.getMetricsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); - EasyMock.expect(mockProvider.getDimensionsRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getIOConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); - EasyMock.expect(mockProvider.getProjectionRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getTuningConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getDeletionRules(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Collections.emptyList()).anyTimes(); EasyMock.replay(mockProvider); @@ -1229,8 +1213,8 @@ public void test_generateAlignedSearchIntervals_failsWhenDefaultGranularityIsCoa new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) )) - .metricsRules(List.of( - new ReindexingMetricsRule("metrics-7d", null, Period.days(7), new AggregatorFactory[0]) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-7d", null, Period.days(7), null, new AggregatorFactory[0], null, null, null) )) .build(); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java index 3fa0796fd60b..e1152ff48266 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ComposingReindexingRuleProvider.java @@ -125,16 +125,6 @@ public List getDeletionRules(Interval interval, DateTime .orElse(Collections.emptyList()); } - @Override - public List getMetricsRules() - { - return providers.stream() - .map(ReindexingRuleProvider::getMetricsRules) - .filter(rules -> !rules.isEmpty()) - .findFirst() - .orElse(Collections.emptyList()); - } - @Override @Nullable public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime) @@ -156,38 +146,6 @@ public List getDataSchemaRules() .orElse(Collections.emptyList()); } - @Override - @Nullable - public ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime) - { - return providers.stream() - .map(p -> p.getMetricsRule(interval, referenceTime)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - @Override - public List getDimensionsRules() - { - return providers.stream() - .map(ReindexingRuleProvider::getDimensionsRules) - .filter(rules -> !rules.isEmpty()) - .findFirst() - .orElse(Collections.emptyList()); - } - - @Override - @Nullable - public ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime) - { - return providers.stream() - .map(p -> p.getDimensionsRule(interval, referenceTime)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - @Override public List getIOConfigRules() { @@ -209,27 +167,6 @@ public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime refere .orElse(null); } - @Override - public List getProjectionRules() - { - return providers.stream() - .map(ReindexingRuleProvider::getProjectionRules) - .filter(rules -> !rules.isEmpty()) - .findFirst() - .orElse(Collections.emptyList()); - } - - @Override - @Nullable - public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime) - { - return providers.stream() - .map(p -> p.getProjectionRule(interval, referenceTime)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - @Override @Nullable public ReindexingSegmentGranularityRule getSegmentGranularityRule(Interval interval, DateTime referenceTime) @@ -251,27 +188,6 @@ public List getSegmentGranularityRules() .orElse(Collections.emptyList()); } - @Override - @Nullable - public ReindexingQueryGranularityRule getQueryGranularityRule(Interval interval, DateTime referenceTime) - { - return providers.stream() - .map(p -> p.getQueryGranularityRule(interval, referenceTime)) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - } - - @Override - public List getQueryGranularityRules() - { - return providers.stream() - .map(ReindexingRuleProvider::getQueryGranularityRules) - .filter(rules -> !rules.isEmpty()) - .findFirst() - .orElse(Collections.emptyList()); - } - @Override public List getTuningConfigRules() { diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index f2996c6d27db..cd64db8aa1bd 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -89,12 +89,8 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider public static final String TYPE = "inline"; private final List reindexingDeletionRules; - private final List reindexingMetricsRules; - private final List reindexingDimensionsRules; private final List reindexingIOConfigRules; - private final List reindexingProjectionRules; private final List reindexingSegmentGranularityRules; - private final List reindexingQueryGranularityRules; private final List reindexingTuningConfigRules; private final List reindexingDataSchemaRules; @@ -102,23 +98,15 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider @JsonCreator public InlineReindexingRuleProvider( @JsonProperty("reindexingDeletionRules") @Nullable List reindexingDeletionRules, - @JsonProperty("reindexingMetricsRules") @Nullable List reindexingMetricsRules, - @JsonProperty("reindexingDimensionsRules") @Nullable List reindexingDimensionsRules, @JsonProperty("reindexingIOConfigRules") @Nullable List reindexingIOConfigRules, - @JsonProperty("reindexingProjectionRules") @Nullable List reindexingProjectionRules, @JsonProperty("reindexingSegmentGranularityRules") @Nullable List reindexingSegmentGranularityRules, - @JsonProperty("reindexingQueryGranularityRules") @Nullable List reindexingQueryGranularityRules, @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules, @JsonProperty("reindexingDataSchemaRules") @Nullable List reindexingDataSchemaRules ) { this.reindexingDeletionRules = Configs.valueOrDefault(reindexingDeletionRules, Collections.emptyList()); - this.reindexingMetricsRules = Configs.valueOrDefault(reindexingMetricsRules, Collections.emptyList()); - this.reindexingDimensionsRules = Configs.valueOrDefault(reindexingDimensionsRules, Collections.emptyList()); this.reindexingIOConfigRules = Configs.valueOrDefault(reindexingIOConfigRules, Collections.emptyList()); - this.reindexingProjectionRules = Configs.valueOrDefault(reindexingProjectionRules, Collections.emptyList()); this.reindexingSegmentGranularityRules = Configs.valueOrDefault(reindexingSegmentGranularityRules, Collections.emptyList()); - this.reindexingQueryGranularityRules = Configs.valueOrDefault(reindexingQueryGranularityRules, Collections.emptyList()); this.reindexingTuningConfigRules = Configs.valueOrDefault(reindexingTuningConfigRules, Collections.emptyList()); this.reindexingDataSchemaRules = Configs.valueOrDefault(reindexingDataSchemaRules, Collections.emptyList()); } @@ -142,13 +130,6 @@ public List getDeletionRules() return reindexingDeletionRules; } - @Override - @JsonProperty("reindexingMetricsRules") - public List getMetricsRules() - { - return reindexingMetricsRules; - } - @Override @Nullable public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime) @@ -163,13 +144,6 @@ public List getDataSchemaRules() return reindexingDataSchemaRules; } - @Override - @JsonProperty("reindexingDimensionsRules") - public List getDimensionsRules() - { - return reindexingDimensionsRules; - } - @Override @JsonProperty("reindexingIOConfigRules") public List getIOConfigRules() @@ -177,20 +151,6 @@ public List getIOConfigRules() return reindexingIOConfigRules; } - @Override - @JsonProperty("reindexingProjectionRules") - public List getProjectionRules() - { - return reindexingProjectionRules; - } - - @Override - @JsonProperty("reindexingQueryGranularityRules") - public List getQueryGranularityRules() - { - return reindexingQueryGranularityRules; - } - @Override @JsonProperty("reindexingSegmentGranularityRules") public List getSegmentGranularityRules() @@ -211,20 +171,6 @@ public List getDeletionRules(Interval interval, DateTime return getApplicableRules(reindexingDeletionRules, interval, referenceTime); } - @Override - @Nullable - public ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime) - { - return getApplicableRule(reindexingMetricsRules, interval, referenceTime); - } - - @Override - @Nullable - public ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime) - { - return getApplicableRule(reindexingDimensionsRules, interval, referenceTime); - } - @Override @Nullable public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime referenceTime) @@ -232,13 +178,6 @@ public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime refere return getApplicableRule(reindexingIOConfigRules, interval, referenceTime); } - @Override - @Nullable - public ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime) - { - return getApplicableRule(reindexingProjectionRules, interval, referenceTime); - } - @Override @Nullable public ReindexingSegmentGranularityRule getSegmentGranularityRule( @@ -249,16 +188,6 @@ public ReindexingSegmentGranularityRule getSegmentGranularityRule( return getApplicableRule(reindexingSegmentGranularityRules, interval, referenceTime); } - @Override - @Nullable - public ReindexingQueryGranularityRule getQueryGranularityRule( - Interval interval, - DateTime referenceTime - ) - { - return getApplicableRule(reindexingQueryGranularityRules, interval, referenceTime); - } - @Override @Nullable public ReindexingTuningConfigRule getTuningConfigRule(Interval interval, DateTime referenceTime) @@ -323,12 +252,8 @@ public boolean equals(Object o) } InlineReindexingRuleProvider that = (InlineReindexingRuleProvider) o; return Objects.equals(reindexingDeletionRules, that.reindexingDeletionRules) - && Objects.equals(reindexingMetricsRules, that.reindexingMetricsRules) - && Objects.equals(reindexingDimensionsRules, that.reindexingDimensionsRules) && Objects.equals(reindexingIOConfigRules, that.reindexingIOConfigRules) - && Objects.equals(reindexingProjectionRules, that.reindexingProjectionRules) && Objects.equals(reindexingSegmentGranularityRules, that.reindexingSegmentGranularityRules) - && Objects.equals(reindexingQueryGranularityRules, that.reindexingQueryGranularityRules) && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules) && Objects.equals(reindexingDataSchemaRules, that.reindexingDataSchemaRules); } @@ -338,12 +263,8 @@ public int hashCode() { return Objects.hash( reindexingDeletionRules, - reindexingMetricsRules, - reindexingDimensionsRules, reindexingIOConfigRules, - reindexingProjectionRules, reindexingSegmentGranularityRules, - reindexingQueryGranularityRules, reindexingTuningConfigRules, reindexingDataSchemaRules ); @@ -354,12 +275,8 @@ public String toString() { return "InlineReindexingRuleProvider{" + "reindexingDeletionRules=" + reindexingDeletionRules - + ", reindexingMetricsRules=" + reindexingMetricsRules - + ", reindexingDimensionsRules=" + reindexingDimensionsRules + ", reindexingIOConfigRules=" + reindexingIOConfigRules - + ", reindexingProjectionRules=" + reindexingProjectionRules + ", reindexingSegmentGranularityRules=" + reindexingSegmentGranularityRules - + ", reindexingQueryGranularityRules=" + reindexingQueryGranularityRules + ", reindexingTuningConfigRules=" + reindexingTuningConfigRules + ", reindexingDataSchemaRules=" + reindexingDataSchemaRules + '}'; @@ -368,12 +285,8 @@ public String toString() public static class Builder { private List reindexingDeletionRules; - private List reindexingMetricsRules; - private List reindexingDimensionsRules; private List reindexingIOConfigRules; - private List reindexingProjectionRules; private List reindexingSegmentGranularityRules; - private List reindexingQueryGranularityRules; private List reindexingTuningConfigRules; private List reindexingDataSchemaRules; @@ -389,42 +302,18 @@ public Builder dataSchemaRules(List reindexingDataSche return this; } - public Builder metricsRules(List reindexingMetricsRules) - { - this.reindexingMetricsRules = reindexingMetricsRules; - return this; - } - - public Builder dimensionsRules(List reindexingDimensionsRules) - { - this.reindexingDimensionsRules = reindexingDimensionsRules; - return this; - } - public Builder ioConfigRules(List reindexingIOConfigRules) { this.reindexingIOConfigRules = reindexingIOConfigRules; return this; } - public Builder projectionRules(List reindexingProjectionRules) - { - this.reindexingProjectionRules = reindexingProjectionRules; - return this; - } - public Builder segmentGranularityRules(List reindexingSegmentGranularityRules) { this.reindexingSegmentGranularityRules = reindexingSegmentGranularityRules; return this; } - public Builder queryGranularityRules(List reindexingQueryGranularityRules) - { - this.reindexingQueryGranularityRules = reindexingQueryGranularityRules; - return this; - } - public Builder tuningConfigRules(List reindexingTuningConfigRules) { this.reindexingTuningConfigRules = reindexingTuningConfigRules; @@ -435,12 +324,8 @@ public InlineReindexingRuleProvider build() { return new InlineReindexingRuleProvider( reindexingDeletionRules, - reindexingMetricsRules, - reindexingDimensionsRules, reindexingIOConfigRules, - reindexingProjectionRules, reindexingSegmentGranularityRules, - reindexingQueryGranularityRules, reindexingTuningConfigRules, reindexingDataSchemaRules ); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index abebf0dc5c58..77cb4968ac2f 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -28,7 +28,6 @@ import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.joda.time.DateTime; import org.joda.time.Interval; diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java deleted file mode 100644 index 64c505b19f14..000000000000 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDimensionsRule.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.compaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.joda.time.Period; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * A {@link ReindexingRule} that specifies a {@link UserCompactionTaskDimensionsConfig} for tasks to configure. - *

    - * This rule defines which dimensions and their types for reindexed segments. For example, - * dropping unused dimensions from older data can reduce storage size. - *

    - * This is a non-additive rule. Multiple dimensions rules cannot be applied to the same interval, - * as a segment can only have one dimensions specification. - *

    - * Example inline usage: - *

    {@code
    - * {
    - *   "id": "optimize-dimensions-90d",
    - *   "olderThan": "P90D",
    - *   "dimensionsSpec": {
    - *     "dimensions": [
    - *       "country",
    - *       "city",
    - *       { "type": "long", "name": "user_id" }
    - *     ]
    - *   },
    - *   "description": "modify dimension schema for data older than 90 days"
    - * }
    - * }
    - */ -public class ReindexingDimensionsRule extends AbstractReindexingRule -{ - private final UserCompactionTaskDimensionsConfig dimensionsSpec; - - @JsonCreator - public ReindexingDimensionsRule( - @JsonProperty("id") @Nonnull String id, - @JsonProperty("description") @Nullable String description, - @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("dimensionsSpec") @Nonnull UserCompactionTaskDimensionsConfig dimensionsSpec - ) - { - super(id, description, olderThan); - this.dimensionsSpec = Objects.requireNonNull(dimensionsSpec, "dimensionsSpec cannot be null"); - } - - @JsonProperty - public UserCompactionTaskDimensionsConfig getDimensionsSpec() - { - return dimensionsSpec; - } -} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java deleted file mode 100644 index 279fd48ae2b1..000000000000 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingMetricsRule.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.compaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.query.aggregation.AggregatorFactory; -import org.joda.time.Period; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * A {@link ReindexingRule} that specifies a {@link AggregatorFactory[]} for tasks to configure. - *

    - * This rule defines the metrics specification used during reindexing, enabling rollup and pre-aggregation - * of older data. For example, applying sum and count aggregators to historical data can significantly - * reduce storage size while preserving queryability for common aggregation queries. - *

    - * This is a non-additive rule. Multiple metrics rules cannot be applied to the same interval, as a segment can only - * have one metrics specification. - *

    - * Example inline usage: - *

    {@code
    - * {
    - *   "id": "rollup-90d",
    - *   "olderThan": "P90D",
    - *   "metricsSpec": [
    - *     { "type": "count", "name": "count" },
    - *     { "type": "longSum", "name": "total_views", "fieldName": "views" }
    - *   ],
    - *   "description": "Enable rollup for data older than 90 days"
    - * }
    - * }
    - */ -public class ReindexingMetricsRule extends AbstractReindexingRule -{ - private final AggregatorFactory[] metricsSpec; - - @JsonCreator - public ReindexingMetricsRule( - @JsonProperty("id") @Nonnull String id, - @JsonProperty("description") @Nullable String description, - @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("metricsSpec") @Nonnull AggregatorFactory[] metricsSpec - ) - { - super(id, description, olderThan); - this.metricsSpec = Objects.requireNonNull(metricsSpec, "metricsSpec cannot be null"); - } - - @JsonProperty - @Nonnull - public AggregatorFactory[] getMetricsSpec() - { - return metricsSpec; - } -} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java deleted file mode 100644 index c4d09f829768..000000000000 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingProjectionRule.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.compaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.data.input.impl.AggregateProjectionSpec; -import org.joda.time.Period; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; -import java.util.Objects; - -/** - * A {@link ReindexingRule} that specifies {@link AggregateProjectionSpec}s to use while building segments. - *

    - * This rule defines pre-aggregated views of data that can accelerate specific query patterns. Projections are - * particularly useful for older data where query patterns are well-understood and storage efficiency is valuable. - *

    - * This is a non-additive rule. - *

    - * Example inline usage: - *

    {@code
    - * {
    - *   "id": "hourly-projection-90d",
    - *   "olderThan": "P90D",
    - *   "projections": [
    - *     {
    - *       "name": "hourly_agg",
    - *       "dimensions": ["country"],
    - *       "metrics": [
    - *         { "type": "longSum", "name": "total_views", "fieldName": "views" }
    - *       ]
    - *     }
    - *   ],
    - *   "description": "create hourly aggregation projection for data older than 90 days"
    - * }
    - * }
    - */ -public class ReindexingProjectionRule extends AbstractReindexingRule -{ - private final List projections; - - @JsonCreator - public ReindexingProjectionRule( - @JsonProperty("id") @Nonnull String id, - @JsonProperty("description") @Nullable String description, - @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("projections") @Nonnull List projections - ) - { - super(id, description, olderThan); - this.projections = Objects.requireNonNull(projections, "projections cannot be null"); - } - - @JsonProperty - public List getProjections() - { - return projections; - } -} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java deleted file mode 100644 index 6721f376a12e..000000000000 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRule.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.compaction; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import org.apache.druid.java.util.common.granularity.Granularity; -import org.joda.time.Period; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Objects; - -/** - * A {@link ReindexingRule} that specifies a query granularity for reindexing tasks to configure. - *

    - * This rule controls the granularity of individual rows written to segments. For example, changing from - * minute-level query granularity to hour-level query granularity can reduce data size and improve query performance - * for older data that doesn't require fine-grained time resolution. - *

    - * This is a non-additive rule. Multiple query granularity rules cannot be applied to the same segment. - *

    - * Example inline usage: - *

    {@code
    - * {
    - *     "id": "hour-30d",
    - *     "olderThan": "P30D",
    - *     "queryGranularity": "HOUR"
    - *     "rollup": true,
    - *     "description": "Rollup to hour query granularity for data older than 30 days"
    - * }
    - * }
    - */ -public class ReindexingQueryGranularityRule extends AbstractReindexingRule -{ - private final Granularity queryGranularity; - private final Boolean rollup; - - @JsonCreator - public ReindexingQueryGranularityRule( - @JsonProperty("id") @Nonnull String id, - @JsonProperty("description") @Nullable String description, - @JsonProperty("olderThan") @Nonnull Period olderThan, - @JsonProperty("queryGranularity") @Nonnull Granularity queryGranularity, - @JsonProperty("rollup") @Nonnull Boolean rollup - ) - { - super(id, description, olderThan); - this.queryGranularity = Objects.requireNonNull(queryGranularity); - this.rollup = Objects.requireNonNull(rollup); - } - - @JsonProperty - public Granularity getQueryGranularity() - { - return queryGranularity; - } - - @JsonProperty - public Boolean getRollup() - { - return rollup; - } -} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java index b259e8d14240..be617169ec5b 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRuleProvider.java @@ -87,26 +87,6 @@ default boolean isReady() */ List getDeletionRules(); - /** - * Returns the matched reindexing metrics rule that applies to the given interval. - *

    - * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider - * implementation and should be clearly documented. - *

    - * - * @param interval The interval to check applicability against. - * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. - * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return {@link ReindexingMetricsRule} rule that applies to the given interval. - */ - @Nullable - ReindexingMetricsRule getMetricsRule(Interval interval, DateTime referenceTime); - - /** - * Returns ALL reindexing metrics rules. - */ - List getMetricsRules(); - /** * Returns the matched reindexing data schema rule that applies to the given interval. *

    @@ -127,26 +107,6 @@ default boolean isReady() */ List getDataSchemaRules(); - /** - * Returns the matched reindexing dimensions rule that applies to the given interval. - *

    - * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider - * implementation and should be clearly documented. - *

    - * - * @param interval The interval to check applicability against. - * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. - * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return {@link ReindexingDimensionsRule} rule that applies to the given interval. - */ - @Nullable - ReindexingDimensionsRule getDimensionsRule(Interval interval, DateTime referenceTime); - - /** - * Returns ALL reindexing dimensions rules. - */ - List getDimensionsRules(); - /** * Returns the matched reindexing IO config rule that applies to the given interval. *

    @@ -167,26 +127,6 @@ default boolean isReady() */ List getIOConfigRules(); - /** - * Returns the matched reindexing projection rule that applies to the given interval. - *

    - * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider - * implementation and should be clearly documented. - *

    - * - * @param interval The interval to check applicability against. - * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. - * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return {@link ReindexingProjectionRule} rule that applies to the given interval. - */ - @Nullable - ReindexingProjectionRule getProjectionRule(Interval interval, DateTime referenceTime); - - /** - * Returns ALL reindexing projection rules. - */ - List getProjectionRules(); - /** * Returns the matched reindexing segment granularity rule that applies to the given interval. *

    @@ -207,26 +147,6 @@ default boolean isReady() */ List getSegmentGranularityRules(); - /** - * Returns the matched reindexing query granularity rule that applies to the given interval. - *

    - * Handling cases of multiple applicable rules and/or partial overlaps is the responsibility of the provider - * implementation and should be clearly documented. - *

    - * - * @param interval The interval to check applicability against. - * @param referenceTime The reference time to use for period calculations while determining rule applicability for an interval. - * e.g., a rule with period P7D applies to data older than 7 days from the reference time. - * @return {@link ReindexingQueryGranularityRule} rule that applies to the given interval. - */ - @Nullable - ReindexingQueryGranularityRule getQueryGranularityRule(Interval interval, DateTime referenceTime); - - /** - * Returns ALL reindexing query granularity rules. - */ - List getQueryGranularityRules(); - /** * Returns the matched reindexing tuning config rule that applies to the given interval. *

    @@ -258,11 +178,7 @@ default boolean isReady() default Stream streamAllRules() { return Stream.of( - getMetricsRules().stream(), - getDimensionsRules().stream(), getIOConfigRules().stream(), - getProjectionRules().stream(), - getQueryGranularityRules().stream(), getTuningConfigRules().stream(), getDeletionRules().stream(), getSegmentGranularityRules().stream(), diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 75167b27f680..7dae689f19ae 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -179,66 +179,6 @@ public void test_getSegmentGranularityRules_compositingBehavior() ); } - @Test - public void test_getQueryGranularityRules_compositingBehavior() - { - testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().queryGranularityRules(rules).build(), - ComposingReindexingRuleProvider::getQueryGranularityRules, - createQueryGranularityRule("rule1", Period.days(7)), - createQueryGranularityRule("rule2", Period.days(30)), - ReindexingQueryGranularityRule::getId - ); - } - - @Test - public void test_getMetricsRules_compositingBehavior() - { - testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().metricsRules(rules).build(), - ComposingReindexingRuleProvider::getMetricsRules, - createMetricsRule("rule1", Period.days(7)), - createMetricsRule("rule2", Period.days(30)), - ReindexingMetricsRule::getId - ); - } - - @Test - public void test_getMetricsRuleWithInterval_compositingBehavior() - { - testComposingBehaviorForNonAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().metricsRules(rules).build(), - (provider, it) -> provider.getMetricsRule(it.interval, it.time), - createMetricsRule("rule1", Period.days(7)), - createMetricsRule("rule2", Period.days(30)), - ReindexingMetricsRule::getId - ); - } - - @Test - public void test_getDimensionsRules_compositingBehavior() - { - testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().dimensionsRules(rules).build(), - ComposingReindexingRuleProvider::getDimensionsRules, - createDimensionsRule("rule1", Period.days(7)), - createDimensionsRule("rule2", Period.days(30)), - ReindexingDimensionsRule::getId - ); - } - - @Test - public void test_getDimensionsRuleWithInterval_compositingBehavior() - { - testComposingBehaviorForNonAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().dimensionsRules(rules).build(), - (provider, it) -> provider.getDimensionsRule(it.interval, it.time), - createDimensionsRule("rule1", Period.days(7)), - createDimensionsRule("rule2", Period.days(30)), - ReindexingDimensionsRule::getId - ); - } - @Test public void test_getIOConfigRules_compositingBehavior() { @@ -263,30 +203,6 @@ public void test_getIOConfigRuleWithInterval_compositingBehavior() ); } - @Test - public void test_getProjectionRules_compositingBehavior() - { - testComposingBehaviorForRuleType( - rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), - ComposingReindexingRuleProvider::getProjectionRules, - createProjectionRule("rule1", Period.days(7)), - createProjectionRule("rule2", Period.days(30)), - ReindexingProjectionRule::getId - ); - } - - @Test - public void test_getProjectionRuleWithInterval_compositingBehavior() - { - testComposingBehaviorForNonAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().projectionRules(rules).build(), - (provider, it) -> provider.getProjectionRule(it.interval, it.time), - createProjectionRule("rule1", Period.days(7)), - createProjectionRule("rule2", Period.days(30)), - ReindexingProjectionRule::getId - ); - } - @Test public void test_getTuningConfigRules_compositingBehavior() { @@ -323,18 +239,6 @@ public void test_getSegmentGranularityRuleWithInterval_compositingBehavior() ); } - @Test - public void test_getQueryGranularityRuleWithInterval_compositingBehavior() - { - testComposingBehaviorForNonAdditiveRuleTypeWithInterval( - rules -> InlineReindexingRuleProvider.builder().queryGranularityRules(rules).build(), - (provider, it) -> provider.getQueryGranularityRule(it.interval, it.time), - createQueryGranularityRule("rule1", Period.days(7)), - createQueryGranularityRule("rule2", Period.days(30)), - ReindexingQueryGranularityRule::getId - ); - } - @Test public void test_equals_sameProviders_returnsTrue() { @@ -521,7 +425,7 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( */ private ReindexingRuleProvider createNotReadyProvider() { - return new InlineReindexingRuleProvider(null, null, null, null, null, null, null, null, null) + return new InlineReindexingRuleProvider(null, null, null, null, null) { @Override public boolean isReady() @@ -552,37 +456,6 @@ private ReindexingSegmentGranularityRule createSegmentGranularityRule(String id, ); } - private ReindexingQueryGranularityRule createQueryGranularityRule(String id, Period period) - { - return new ReindexingQueryGranularityRule( - id, - "Test granularity rule", - period, - Granularities.DAY, - false - ); - } - - private ReindexingMetricsRule createMetricsRule(String id, Period period) - { - return new ReindexingMetricsRule( - id, - "Test metrics rule", - period, - new AggregatorFactory[]{new CountAggregatorFactory("count")} - ); - } - - private ReindexingDimensionsRule createDimensionsRule(String id, Period period) - { - return new ReindexingDimensionsRule( - id, - "Test dimensions rule", - period, - new UserCompactionTaskDimensionsConfig(null) - ); - } - private ReindexingIOConfigRule createIOConfigRule(String id, Period period) { return new ReindexingIOConfigRule( @@ -593,23 +466,6 @@ private ReindexingIOConfigRule createIOConfigRule(String id, Period period) ); } - private ReindexingProjectionRule createProjectionRule(String id, Period period) - { - AggregateProjectionSpec projectionSpec = new AggregateProjectionSpec( - "test_projection", - null, - null, - null, - new AggregatorFactory[]{new CountAggregatorFactory("count")} - ); - return new ReindexingProjectionRule( - id, - "Test projection rule", - period, - ImmutableList.of(projectionSpec) - ); - } - private ReindexingTuningConfigRule createTuningConfigRule(String id, Period period) { return new ReindexingTuningConfigRule( diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index ba27044e9571..71dff44b9e6e 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -59,10 +59,6 @@ public class InlineReindexingRuleProviderTest public void test_constructor_nullListsDefaultToEmpty() { InlineReindexingRuleProvider provider = new InlineReindexingRuleProvider( - null, - null, - null, - null, null, null, null, @@ -72,18 +68,10 @@ public void test_constructor_nullListsDefaultToEmpty() Assert.assertNotNull(provider.getDeletionRules()); Assert.assertTrue(provider.getDeletionRules().isEmpty()); - Assert.assertNotNull(provider.getMetricsRules()); - Assert.assertTrue(provider.getMetricsRules().isEmpty()); - Assert.assertNotNull(provider.getDimensionsRules()); - Assert.assertTrue(provider.getDimensionsRules().isEmpty()); Assert.assertNotNull(provider.getIOConfigRules()); Assert.assertTrue(provider.getIOConfigRules().isEmpty()); - Assert.assertNotNull(provider.getProjectionRules()); - Assert.assertTrue(provider.getProjectionRules().isEmpty()); Assert.assertNotNull(provider.getSegmentGranularityRules()); Assert.assertTrue(provider.getSegmentGranularityRules().isEmpty()); - Assert.assertNotNull(provider.getQueryGranularityRules()); - Assert.assertTrue(provider.getQueryGranularityRules().isEmpty()); Assert.assertNotNull(provider.getTuningConfigRules()); Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); Assert.assertTrue(provider.getDataSchemaRules().isEmpty()); @@ -117,22 +105,6 @@ public void test_reindexingRules_validateAdditivity() @Test public void test_allNonAdditiveRules_validateNonAdditivity() { - // Test metrics rules - testNonAdditivity( - "metrics", - this::createMetricsRule, - InlineReindexingRuleProvider.Builder::metricsRules, - InlineReindexingRuleProvider::getMetricsRule - ); - - // Test dimensions rules - testNonAdditivity( - "dimensions", - this::createDimensionsRule, - InlineReindexingRuleProvider.Builder::dimensionsRules, - InlineReindexingRuleProvider::getDimensionsRule - ); - // Test IOConfig rules testNonAdditivity( "ioConfig", @@ -141,14 +113,6 @@ public void test_allNonAdditiveRules_validateNonAdditivity() InlineReindexingRuleProvider::getIOConfigRule ); - // Test projection rules - testNonAdditivity( - "projection", - this::createProjectionRule, - InlineReindexingRuleProvider.Builder::projectionRules, - InlineReindexingRuleProvider::getProjectionRule - ); - // Test segment granularity rules testNonAdditivity( "segmentGranularity", @@ -157,14 +121,6 @@ public void test_allNonAdditiveRules_validateNonAdditivity() InlineReindexingRuleProvider::getSegmentGranularityRule ); - // Test query granularity rules - testNonAdditivity( - "queryGranularity", - this::createQueryGranularityRule, - InlineReindexingRuleProvider.Builder::queryGranularityRules, - InlineReindexingRuleProvider::getQueryGranularityRule - ); - // Test tuning config rules testNonAdditivity( "tuningConfig", @@ -185,23 +141,15 @@ public void test_allNonAdditiveRules_validateNonAdditivity() public void test_allRuleTypesWireCorrectly_withInterval() { ReindexingDeletionRule filterRule = createFilterRule("filter", Period.days(30)); - ReindexingMetricsRule metricsRule = createMetricsRule("metrics", Period.days(30)); - ReindexingDimensionsRule dimensionsRule = createDimensionsRule("dimensions", Period.days(30)); ReindexingIOConfigRule ioConfigRule = createIOConfigRule("ioconfig", Period.days(30)); - ReindexingProjectionRule projectionRule = createProjectionRule("projection", Period.days(30)); ReindexingSegmentGranularityRule segmentGranularityRule = createSegmentGranularityRule("segmentGranularity", Period.days(30)); - ReindexingQueryGranularityRule queryGranularityRule = createQueryGranularityRule("queryGranularity", Period.days(30)); ReindexingTuningConfigRule tuningConfigRule = createTuningConfigRule("tuning", Period.days(30)); ReindexingDataSchemaRule dataSchemaRule = createDataSchemaRule("dataSchema", Period.days(30)); InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .deletionRules(ImmutableList.of(filterRule)) - .metricsRules(ImmutableList.of(metricsRule)) - .dimensionsRules(ImmutableList.of(dimensionsRule)) .ioConfigRules(ImmutableList.of(ioConfigRule)) - .projectionRules(ImmutableList.of(projectionRule)) .segmentGranularityRules(ImmutableList.of(segmentGranularityRule)) - .queryGranularityRules(ImmutableList.of(queryGranularityRule)) .tuningConfigRules(ImmutableList.of(tuningConfigRule)) .dataSchemaRules(ImmutableList.of(dataSchemaRule)) .build(); @@ -209,18 +157,10 @@ public void test_allRuleTypesWireCorrectly_withInterval() Assert.assertEquals(1, provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); Assert.assertEquals("filter", provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); - Assert.assertEquals("metrics", provider.getMetricsRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - - Assert.assertEquals("dimensions", provider.getDimensionsRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("ioconfig", provider.getIOConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("projection", provider.getProjectionRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("segmentGranularity", provider.getSegmentGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("queryGranularity", provider.getQueryGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); Assert.assertEquals("dataSchema", provider.getDataSchemaRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); @@ -290,38 +230,11 @@ private ReindexingDeletionRule createFilterRule(String id, Period period) return new ReindexingDeletionRule(id, null, period, new SelectorDimFilter("dim", "val", null), null); } - private ReindexingMetricsRule createMetricsRule(String id, Period period) - { - return new ReindexingMetricsRule( - id, - null, - period, - new AggregatorFactory[]{new CountAggregatorFactory("count")} - ); - } - - private ReindexingDimensionsRule createDimensionsRule(String id, Period period) - { - return new ReindexingDimensionsRule(id, null, period, new UserCompactionTaskDimensionsConfig(null)); - } - private ReindexingIOConfigRule createIOConfigRule(String id, Period period) { return new ReindexingIOConfigRule(id, null, period, new UserCompactionTaskIOConfig(null)); } - private ReindexingProjectionRule createProjectionRule(String id, Period period) - { - AggregateProjectionSpec projectionSpec = new AggregateProjectionSpec( - "test_projection", - null, - null, - null, - new AggregatorFactory[]{new CountAggregatorFactory("count")} - ); - return new ReindexingProjectionRule(id, null, period, ImmutableList.of(projectionSpec)); - } - private ReindexingSegmentGranularityRule createSegmentGranularityRule(String id, Period period) { return new ReindexingSegmentGranularityRule( @@ -332,17 +245,6 @@ private ReindexingSegmentGranularityRule createSegmentGranularityRule(String id, ); } - private ReindexingQueryGranularityRule createQueryGranularityRule(String id, Period period) - { - return new ReindexingQueryGranularityRule( - id, - null, - period, - Granularities.DAY, - true - ); - } - private ReindexingTuningConfigRule createTuningConfigRule(String id, Period period) { return new ReindexingTuningConfigRule( diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java deleted file mode 100644 index 1c36b1bb86bc..000000000000 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDimensionsRuleTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.compaction; - -import org.apache.druid.java.util.common.DateTimes; -import org.apache.druid.java.util.common.Intervals; -import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; - -public class ReindexingDimensionsRuleTest -{ - private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); - private static final Period PERIOD_14_DAYS = Period.days(14); - - private final ReindexingDimensionsRule rule = new ReindexingDimensionsRule( - "test-dimensions-rule", - "Custom dimensions config", - PERIOD_14_DAYS, - new UserCompactionTaskDimensionsConfig(null) - ); - - @Test - public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() - { - // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) - // Interval ends at 2025-12-03, which is fully before threshold - Interval interval = Intervals.of("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalEndsAtThreshold_returnsFull() - { - // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) - // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = Intervals.of("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalSpansThreshold_returnsPartial() - { - // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) - // Interval starts before threshold and ends after - PARTIAL - Interval interval = Intervals.of("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); - } - - @Test - public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() - { - // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) - // Interval starts after threshold - NONE - Interval interval = Intervals.of("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); - } - - @Test - public void test_getDimensionsSpec_returnsConfiguredValue() - { - UserCompactionTaskDimensionsConfig spec = rule.getDimensionsSpec(); - - Assert.assertNotNull(spec); - } - - @Test - public void test_getId_returnsConfiguredId() - { - Assert.assertEquals("test-dimensions-rule", rule.getId()); - } - - @Test - public void test_getDescription_returnsConfiguredDescription() - { - Assert.assertEquals("Custom dimensions config", rule.getDescription()); - } - - @Test - public void test_getOlderThan_returnsConfiguredPeriod() - { - Assert.assertEquals(PERIOD_14_DAYS, rule.getOlderThan()); - } - - @Test - public void test_constructor_nullId_throwsNullPointerException() - { - UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingDimensionsRule(null, "description", PERIOD_14_DAYS, config) - ); - } - - @Test - public void test_constructor_nullPeriod_throwsNullPointerException() - { - UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingDimensionsRule("test-id", "description", null, config) - ); - } - - @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() - { - UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); - Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingDimensionsRule("test-id", "description", zeroPeriod, config) - ); - } - - @Test - public void test_constructor_negativePeriod_throwsIllegalArgumentException() - { - UserCompactionTaskDimensionsConfig config = new UserCompactionTaskDimensionsConfig(null); - Period negativePeriod = Period.days(-14); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingDimensionsRule("test-id", "description", negativePeriod, config) - ); - } - - @Test - public void test_constructor_nullDimensionsSpec_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingDimensionsRule("test-id", "description", PERIOD_14_DAYS, null) - ); - } -} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java deleted file mode 100644 index 0e87f94445ce..000000000000 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingMetricsRuleTest.java +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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.compaction; - -import org.apache.druid.java.util.common.DateTimes; -import org.apache.druid.java.util.common.Intervals; -import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.aggregation.CountAggregatorFactory; -import org.apache.druid.query.aggregation.LongSumAggregatorFactory; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; - -public class ReindexingMetricsRuleTest -{ - private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); - private static final Period PERIOD_90_DAYS = Period.days(90); - - private final AggregatorFactory[] testMetrics = new AggregatorFactory[]{ - new CountAggregatorFactory("count"), - new LongSumAggregatorFactory("total_value", "value") - }; - - private final ReindexingMetricsRule rule = new ReindexingMetricsRule( - "test-metrics-rule", - "Aggregate metrics for old data", - PERIOD_90_DAYS, - testMetrics - ); - - @Test - public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() - { - // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) - // Interval ends at 2025-09-15, which is fully before threshold - Interval interval = Intervals.of("2025-09-14T00:00:00Z/2025-09-15T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalEndsAtThreshold_returnsFull() - { - // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) - // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = Intervals.of("2025-09-19T12:00:00Z/2025-09-20T12:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalSpansThreshold_returnsPartial() - { - // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) - // Interval starts before threshold and ends after - PARTIAL - Interval interval = Intervals.of("2025-09-19T00:00:00Z/2025-09-21T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); - } - - @Test - public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() - { - // Threshold is 2025-09-20T12:00:00Z (90 days before reference time) - // Interval starts after threshold - NONE - Interval interval = Intervals.of("2025-12-01T00:00:00Z/2025-12-02T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); - } - - @Test - public void test_getMetricsSpec_returnsConfiguredMetrics() - { - AggregatorFactory[] metrics = rule.getMetricsSpec(); - - Assert.assertNotNull(metrics); - Assert.assertEquals(2, metrics.length); - Assert.assertEquals("count", metrics[0].getName()); - Assert.assertEquals("total_value", metrics[1].getName()); - } - - @Test - public void test_getId_returnsConfiguredId() - { - Assert.assertEquals("test-metrics-rule", rule.getId()); - } - - @Test - public void test_getDescription_returnsConfiguredDescription() - { - Assert.assertEquals("Aggregate metrics for old data", rule.getDescription()); - } - - @Test - public void test_getOlderThan_returnsConfiguredPeriod() - { - Assert.assertEquals(PERIOD_90_DAYS, rule.getOlderThan()); - } - - @Test - public void test_constructor_nullId_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingMetricsRule(null, "description", PERIOD_90_DAYS, testMetrics) - ); - } - - @Test - public void test_constructor_nullPeriod_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingMetricsRule("test-id", "description", null, testMetrics) - ); - } - - @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() - { - Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingMetricsRule("test-id", "description", zeroPeriod, testMetrics) - ); - } - - @Test - public void test_constructor_negativePeriod_throwsIllegalArgumentException() - { - Period negativePeriod = Period.days(-90); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingMetricsRule("test-id", "description", negativePeriod, testMetrics) - ); - } - - @Test - public void test_constructor_nullMetricsSpec_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingMetricsRule("test-id", "description", PERIOD_90_DAYS, null) - ); - } -} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java deleted file mode 100644 index 0f1d4dfe2cf1..000000000000 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingProjectionRuleTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.compaction; - -import org.apache.druid.java.util.common.DateTimes; -import org.apache.druid.java.util.common.Intervals; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; - -import java.util.Collections; - -public class ReindexingProjectionRuleTest -{ - private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); - private static final Period PERIOD_45_DAYS = Period.days(45); - - private final ReindexingProjectionRule rule = new ReindexingProjectionRule( - "test-projection-rule", - "Add aggregate projections", - PERIOD_45_DAYS, - Collections.emptyList() - ); - - @Test - public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() - { - // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) - // Interval ends at 2025-11-01, which is fully before threshold - Interval interval = Intervals.of("2025-10-31T00:00:00Z/2025-11-01T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalEndsAtThreshold_returnsFull() - { - // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) - // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = Intervals.of("2025-11-03T12:00:00Z/2025-11-04T12:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalSpansThreshold_returnsPartial() - { - // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) - // Interval starts before threshold and ends after - PARTIAL - Interval interval = Intervals.of("2025-11-03T00:00:00Z/2025-11-05T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); - } - - @Test - public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() - { - // Threshold is 2025-11-04T12:00:00Z (45 days before reference time) - // Interval starts after threshold - NONE - Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); - } - - @Test - public void test_getProjections_returnsConfiguredValue() - { - Assert.assertNotNull(rule.getProjections()); - Assert.assertTrue(rule.getProjections().isEmpty()); - } - - @Test - public void test_getId_returnsConfiguredId() - { - Assert.assertEquals("test-projection-rule", rule.getId()); - } - - @Test - public void test_getDescription_returnsConfiguredDescription() - { - Assert.assertEquals("Add aggregate projections", rule.getDescription()); - } - - @Test - public void test_getOlderThan_returnsConfiguredPeriod() - { - Assert.assertEquals(PERIOD_45_DAYS, rule.getOlderThan()); - } - - @Test - public void test_constructor_nullId_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingProjectionRule(null, "description", PERIOD_45_DAYS, Collections.emptyList()) - ); - } - - @Test - public void test_constructor_nullPeriod_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingProjectionRule("test-id", "description", null, Collections.emptyList()) - ); - } - - @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() - { - Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingProjectionRule("test-id", "description", zeroPeriod, Collections.emptyList()) - ); - } - - @Test - public void test_constructor_negativePeriod_throwsIllegalArgumentException() - { - Period negativePeriod = Period.days(-45); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingProjectionRule("test-id", "description", negativePeriod, Collections.emptyList()) - ); - } - - @Test - public void test_constructor_nullProjections_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingProjectionRule("test-id", "description", PERIOD_45_DAYS, null) - ); - } -} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java deleted file mode 100644 index 815bd1afe70c..000000000000 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingQueryGranularityRuleTest.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * 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.compaction; - -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.granularity.Granularity; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; - -public class ReindexingQueryGranularityRuleTest -{ - private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); - private static final Period PERIOD_7_DAYS = Period.days(7); - - private final ReindexingQueryGranularityRule rule = new ReindexingQueryGranularityRule( - "test-rule", - "Test query granularity rule", - PERIOD_7_DAYS, - Granularities.HOUR, - true - ); - - @Test - public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() - { - // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) - // Interval ends at 2025-12-10, which is fully before threshold - Interval interval = Intervals.of("2025-12-09T00:00:00Z/2025-12-10T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalEndsAtThreshold_returnsFull() - { - // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) - // Interval ends exactly at threshold - should be FULL (boundary case) - Interval interval = Intervals.of("2025-12-11T12:00:00Z/2025-12-12T12:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); - } - - @Test - public void test_appliesTo_intervalSpansThreshold_returnsPartial() - { - // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) - // Interval starts before threshold and ends after - PARTIAL - Interval interval = Intervals.of("2025-12-11T00:00:00Z/2025-12-13T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); - } - - @Test - public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() - { - // Threshold is 2025-12-12T12:00:00Z (7 days before reference time) - // Interval starts after threshold - NONE - Interval interval = Intervals.of("2025-12-18T00:00:00Z/2025-12-19T00:00:00Z"); - - ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); - } - - @Test - public void test_getQueryGranularity_returnsConfiguredValue() - { - Granularity granularity = rule.getQueryGranularity(); - - Assert.assertNotNull(granularity); - Assert.assertEquals(Granularities.HOUR, granularity); - } - - @Test - public void test_getRollup_returnsConfiguredValue() - { - Boolean rollup = rule.getRollup(); - - Assert.assertNotNull(rollup); - Assert.assertEquals(true, rollup); - } - - @Test - public void test_getRollup_returnsFalse_whenConfiguredFalse() - { - ReindexingQueryGranularityRule ruleWithoutRollup = new ReindexingQueryGranularityRule( - "test-rule-no-rollup", - "Test query granularity rule without rollup", - PERIOD_7_DAYS, - Granularities.HOUR, - false - ); - - Boolean rollup = ruleWithoutRollup.getRollup(); - - Assert.assertNotNull(rollup); - Assert.assertEquals(false, rollup); - } - - @Test - public void test_getId_returnsConfiguredId() - { - Assert.assertEquals("test-rule", rule.getId()); - } - - @Test - public void test_getDescription_returnsConfiguredDescription() - { - Assert.assertEquals("Test query granularity rule", rule.getDescription()); - } - - @Test - public void test_getOlderThan_returnsConfiguredPeriod() - { - Assert.assertEquals(PERIOD_7_DAYS, rule.getOlderThan()); - } - - @Test - public void test_constructor_nullId_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingQueryGranularityRule(null, "description", PERIOD_7_DAYS, Granularities.HOUR, true) - ); - } - - @Test - public void test_constructor_nullPeriod_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingQueryGranularityRule("test-id", "description", null, Granularities.HOUR, true) - ); - } - - @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() - { - Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingQueryGranularityRule("test-id", "description", zeroPeriod, Granularities.HOUR, true) - ); - } - - @Test - public void test_constructor_negativePeriod_throwsIllegalArgumentException() - { - Period negativePeriod = Period.days(-7); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingQueryGranularityRule("test-id", "description", negativePeriod, Granularities.HOUR, true) - ); - } - - @Test - public void test_constructor_nullQueryGranularity_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingQueryGranularityRule("test-id", "description", PERIOD_7_DAYS, null, true) - ); - } - - @Test - public void test_constructor_nullRollup_throwsNullPointerException() - { - Assert.assertThrows( - NullPointerException.class, - () -> new ReindexingQueryGranularityRule("test-id", "description", PERIOD_7_DAYS, Granularities.HOUR, null) - ); - } -} From f9d020f737f7585a7b85e865cd300e39a5adc6bb Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 13 Feb 2026 16:25:27 -0600 Subject: [PATCH 53/90] checkpointed api for getting a rule timeline --- .../compact/CascadingReindexingTemplate.java | 156 ++++- .../compact/CompactionSupervisor.java | 16 + .../compact/CompactionTimelineView.java | 467 +++++++++++++++ ...ranularityTimelineValidationException.java | 81 +++ .../supervisor/SupervisorResource.java | 79 +++ .../compaction-timeline.scss | 202 +++++++ .../compaction-timeline.tsx | 548 ++++++++++++++++++ web-console/src/components/index.ts | 1 + .../supervisor-table-action-dialog.tsx | 11 +- 9 files changed, 1551 insertions(+), 10 deletions(-) create mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java create mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java create mode 100644 web-console/src/components/compaction-timeline/compaction-timeline.scss create mode 100644 web-console/src/components/compaction-timeline/compaction-timeline.tsx diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index fafcfeae7f8b..503f4bec53aa 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -37,9 +37,13 @@ import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.compaction.ReindexingConfigBuilder; +import org.apache.druid.server.compaction.ReindexingDataSchemaRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -272,6 +276,146 @@ private static boolean shouldOptimizeFilterRules( return filter instanceof NotDimFilter; } + /** + * Generates a timeline view showing the search intervals and their associated compaction + * configurations. This is useful for operators to understand how rules are applied across + * different time periods without actually creating compaction jobs. + * + * @param referenceTime the reference time to use for computing rule periods (typically DateTime.now()) + * @return a view of the compaction timeline with intervals and their configs + */ + public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) + { + if (!ruleProvider.isReady()) { + LOG.info( + "Rule provider [%s] is not ready, returning empty timeline for dataSource[%s]", + ruleProvider.getType(), + dataSource + ); + return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); + } + + List searchIntervals; + try { + searchIntervals = generateAlignedSearchIntervals(referenceTime); + } + catch (GranularityTimelineValidationException e) { + // Validation failed - extract structured error details and return with validation error + LOG.warn(e, "Validation failed for compaction timeline of dataSource[%s]", dataSource); + CompactionTimelineView.ValidationError validationError = new CompactionTimelineView.ValidationError( + "INVALID_GRANULARITY_TIMELINE", + e.getMessage(), + e.getOlderInterval().toString(), + e.getOlderGranularity().toString(), + e.getNewerInterval().toString(), + e.getNewerGranularity().toString() + ); + return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); + } + catch (IAE e) { + // Other validation errors (e.g., no rules configured) + LOG.warn(e, "Validation failed for compaction timeline of dataSource[%s]", dataSource); + CompactionTimelineView.ValidationError validationError = new CompactionTimelineView.ValidationError( + "VALIDATION_ERROR", + e.getMessage(), + null, + null, + null, + null + ); + return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); + } + + if (searchIntervals.isEmpty()) { + LOG.warn("No search intervals generated for dataSource[%s]", dataSource); + return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); + } + + // Calculate effective end time based on skip offset + DateTime effectiveEndTime = referenceTime; + CompactionTimelineView.SkipOffsetInfo skipOffsetInfo = null; + + if (skipOffsetFromNow != null) { + effectiveEndTime = referenceTime.minus(skipOffsetFromNow); + CompactionTimelineView.AppliedSkipOffset applied = new CompactionTimelineView.AppliedSkipOffset( + "skipOffsetFromNow", + skipOffsetFromNow, + effectiveEndTime + ); + skipOffsetInfo = new CompactionTimelineView.SkipOffsetInfo(applied, null); + } else if (skipOffsetFromLatest != null) { + // skipOffsetFromLatest requires actual timeline data, so we can't apply it in preview mode + CompactionTimelineView.NotAppliedSkipOffset notApplied = new CompactionTimelineView.NotAppliedSkipOffset( + "skipOffsetFromLatest", + skipOffsetFromLatest, + "Requires actual segment timeline data" + ); + skipOffsetInfo = new CompactionTimelineView.SkipOffsetInfo(null, notApplied); + } + + // Build configs for each interval + List intervalConfigs = new ArrayList<>(); + for (Interval searchInterval : searchIntervals) { + // Clamp interval to effective end time + Interval clampedInterval = searchInterval; + if (searchInterval.getEnd().isAfter(effectiveEndTime)) { + if (searchInterval.getStart().isBefore(effectiveEndTime)) { + clampedInterval = new Interval(searchInterval.getStart(), effectiveEndTime); + } else { + // Entire interval is beyond skip offset, skip it + continue; + } + } + + InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( + ruleProvider, + defaultSegmentGranularity, + clampedInterval, + referenceTime + ); + int ruleCount = configBuilder.applyTo(builder); + + if (ruleCount > 0) { + // Collect the ACTUAL rules that were applied (matching ReindexingConfigBuilder logic) + List appliedRules = new ArrayList<>(); + + ReindexingTuningConfigRule tuningRule = ruleProvider.getTuningConfigRule(clampedInterval, referenceTime); + if (tuningRule != null) { + appliedRules.add(tuningRule); + } + + ReindexingIOConfigRule ioConfigRule = ruleProvider.getIOConfigRule(clampedInterval, referenceTime); + if (ioConfigRule != null) { + appliedRules.add(ioConfigRule); + } + + ReindexingDataSchemaRule dataSchemaRule = ruleProvider.getDataSchemaRule(clampedInterval, referenceTime); + if (dataSchemaRule != null) { + appliedRules.add(dataSchemaRule); + } + + // Deletion rules are additive - collect all that apply + List deletionRules = ruleProvider.getDeletionRules(clampedInterval, referenceTime); + appliedRules.addAll(deletionRules); + + ReindexingSegmentGranularityRule segmentGranularityRule = ruleProvider.getSegmentGranularityRule(clampedInterval, referenceTime); + if (segmentGranularityRule != null) { + appliedRules.add(segmentGranularityRule); + } + + intervalConfigs.add(new CompactionTimelineView.IntervalConfig( + clampedInterval, + ruleCount, + builder.build(), + appliedRules + )); + } + } + + return new CompactionTimelineView(dataSource, referenceTime, skipOffsetInfo, intervalConfigs, null); + } + @Override public List createCompactionJobs( DruidInputSource source, @@ -675,16 +819,12 @@ private void validateSegmentGranularityTimeline(List ti // If the older interval's granularity is finer than the newer interval's granularity, // that means we're getting coarser as we move toward present, which is invalid. if (olderGran.isFinerThan(newerGran)) { - throw new IAE( - "Invalid segment granularity timeline for dataSource[%s]: " - + "Interval[%s] with granularity[%s] is more recent than " - + "interval[%s] with granularity[%s], but has a coarser granularity. " - + "Segment granularity must stay the same or become finer as data ages from present to past.", + throw new GranularityTimelineValidationException( dataSource, - newerInterval.interval, - newerGran, olderInterval.interval, - olderGran + olderGran, + newerInterval.interval, + newerGran ); } } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java index f9ba0eee6db7..7f85573c0260 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java @@ -78,6 +78,22 @@ public List createJobs( return supervisorSpec.getTemplate().createCompactionJobs(inputSource, jobParams); } + /** + * Gets a timeline view of the compaction intervals and their associated configurations. + * This is only supported for CascadingReindexingTemplate-based supervisors. + * + * @param referenceTime the reference time to use for computing rule periods + * @return Optional containing the timeline view if this is a cascading reindexing supervisor, empty otherwise + */ + public java.util.Optional getCompactionTimelineView(org.joda.time.DateTime referenceTime) + { + CompactionJobTemplate template = supervisorSpec.getTemplate(); + if (template instanceof CascadingReindexingTemplate) { + return java.util.Optional.of(((CascadingReindexingTemplate) template).getCompactionTimelineView(referenceTime)); + } + return java.util.Optional.empty(); + } + @Override public void start() { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java new file mode 100644 index 000000000000..4d242ea78012 --- /dev/null +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java @@ -0,0 +1,467 @@ +/* + * 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.indexing.compact; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.server.compaction.ReindexingRule; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; + +/** + * Represents the timeline of search intervals and their associated compaction configurations + * for a cascading reindexing supervisor. This view helps operators understand how different + * compaction rules are applied across time intervals. + */ +public class CompactionTimelineView +{ + private final String dataSource; + private final DateTime referenceTime; + private final SkipOffsetInfo skipOffset; + private final List intervals; + private final ValidationError validationError; + + @JsonCreator + public CompactionTimelineView( + @JsonProperty("dataSource") String dataSource, + @JsonProperty("referenceTime") DateTime referenceTime, + @JsonProperty("skipOffset") @Nullable SkipOffsetInfo skipOffset, + @JsonProperty("intervals") List intervals, + @JsonProperty("validationError") @Nullable ValidationError validationError + ) + { + this.dataSource = dataSource; + this.referenceTime = referenceTime; + this.skipOffset = skipOffset; + this.intervals = intervals; + this.validationError = validationError; + } + + @JsonProperty + public String getDataSource() + { + return dataSource; + } + + @JsonProperty + public DateTime getReferenceTime() + { + return referenceTime; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public SkipOffsetInfo getSkipOffset() + { + return skipOffset; + } + + @JsonProperty + public List getIntervals() + { + return intervals; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public ValidationError getValidationError() + { + return validationError; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CompactionTimelineView that = (CompactionTimelineView) o; + return Objects.equals(dataSource, that.dataSource) && + Objects.equals(referenceTime, that.referenceTime) && + Objects.equals(skipOffset, that.skipOffset) && + Objects.equals(intervals, that.intervals) && + Objects.equals(validationError, that.validationError); + } + + @Override + public int hashCode() + { + return Objects.hash(dataSource, referenceTime, skipOffset, intervals, validationError); + } + + /** + * Information about a validation error that occurred while building the timeline. + */ + public static class ValidationError + { + private final String errorType; + private final String message; + private final String olderInterval; + private final String olderGranularity; + private final String newerInterval; + private final String newerGranularity; + + @JsonCreator + public ValidationError( + @JsonProperty("errorType") String errorType, + @JsonProperty("message") String message, + @JsonProperty("olderInterval") @Nullable String olderInterval, + @JsonProperty("olderGranularity") @Nullable String olderGranularity, + @JsonProperty("newerInterval") @Nullable String newerInterval, + @JsonProperty("newerGranularity") @Nullable String newerGranularity + ) + { + this.errorType = errorType; + this.message = message; + this.olderInterval = olderInterval; + this.olderGranularity = olderGranularity; + this.newerInterval = newerInterval; + this.newerGranularity = newerGranularity; + } + + @JsonProperty + public String getErrorType() + { + return errorType; + } + + @JsonProperty + public String getMessage() + { + return message; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public String getOlderInterval() + { + return olderInterval; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public String getOlderGranularity() + { + return olderGranularity; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public String getNewerInterval() + { + return newerInterval; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public String getNewerGranularity() + { + return newerGranularity; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidationError that = (ValidationError) o; + return Objects.equals(errorType, that.errorType) && + Objects.equals(message, that.message) && + Objects.equals(olderInterval, that.olderInterval) && + Objects.equals(olderGranularity, that.olderGranularity) && + Objects.equals(newerInterval, that.newerInterval) && + Objects.equals(newerGranularity, that.newerGranularity); + } + + @Override + public int hashCode() + { + return Objects.hash(errorType, message, olderInterval, olderGranularity, newerInterval, newerGranularity); + } + } + + /** + * Information about skip offsets and whether they were applied. + */ + public static class SkipOffsetInfo + { + private final AppliedSkipOffset applied; + private final NotAppliedSkipOffset notApplied; + + @JsonCreator + public SkipOffsetInfo( + @JsonProperty("applied") @Nullable AppliedSkipOffset applied, + @JsonProperty("notApplied") @Nullable NotAppliedSkipOffset notApplied + ) + { + this.applied = applied; + this.notApplied = notApplied; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public AppliedSkipOffset getApplied() + { + return applied; + } + + @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) + @Nullable + public NotAppliedSkipOffset getNotApplied() + { + return notApplied; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SkipOffsetInfo that = (SkipOffsetInfo) o; + return Objects.equals(applied, that.applied) && + Objects.equals(notApplied, that.notApplied); + } + + @Override + public int hashCode() + { + return Objects.hash(applied, notApplied); + } + } + + /** + * Information about a skip offset that was applied. + */ + public static class AppliedSkipOffset + { + private final String type; + private final Period period; + private final DateTime effectiveEndTime; + + @JsonCreator + public AppliedSkipOffset( + @JsonProperty("type") String type, + @JsonProperty("period") Period period, + @JsonProperty("effectiveEndTime") DateTime effectiveEndTime + ) + { + this.type = type; + this.period = period; + this.effectiveEndTime = effectiveEndTime; + } + + @JsonProperty + public String getType() + { + return type; + } + + @JsonProperty + public Period getPeriod() + { + return period; + } + + @JsonProperty + public DateTime getEffectiveEndTime() + { + return effectiveEndTime; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AppliedSkipOffset that = (AppliedSkipOffset) o; + return Objects.equals(type, that.type) && + Objects.equals(period, that.period) && + Objects.equals(effectiveEndTime, that.effectiveEndTime); + } + + @Override + public int hashCode() + { + return Objects.hash(type, period, effectiveEndTime); + } + } + + /** + * Information about a skip offset that was not applied. + */ + public static class NotAppliedSkipOffset + { + private final String type; + private final Period period; + private final String reason; + + @JsonCreator + public NotAppliedSkipOffset( + @JsonProperty("type") String type, + @JsonProperty("period") Period period, + @JsonProperty("reason") String reason + ) + { + this.type = type; + this.period = period; + this.reason = reason; + } + + @JsonProperty + public String getType() + { + return type; + } + + @JsonProperty + public Period getPeriod() + { + return period; + } + + @JsonProperty + public String getReason() + { + return reason; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NotAppliedSkipOffset that = (NotAppliedSkipOffset) o; + return Objects.equals(type, that.type) && + Objects.equals(period, that.period) && + Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() + { + return Objects.hash(type, period, reason); + } + } + + /** + * Represents a search interval and its associated compaction configuration. + */ + public static class IntervalConfig + { + private final Interval interval; + private final int ruleCount; + private final DataSourceCompactionConfig config; + private final List appliedRules; + + @JsonCreator + public IntervalConfig( + @JsonProperty("interval") Interval interval, + @JsonProperty("ruleCount") int ruleCount, + @JsonProperty("config") DataSourceCompactionConfig config, + @JsonProperty("appliedRules") List appliedRules + ) + { + this.interval = interval; + this.ruleCount = ruleCount; + this.config = config; + this.appliedRules = appliedRules; + } + + @JsonProperty + public Interval getInterval() + { + return interval; + } + + @JsonProperty + public int getRuleCount() + { + return ruleCount; + } + + @JsonProperty + public DataSourceCompactionConfig getConfig() + { + return config; + } + + @JsonProperty + public List getAppliedRules() + { + return appliedRules; + } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IntervalConfig that = (IntervalConfig) o; + return ruleCount == that.ruleCount && + Objects.equals(interval, that.interval) && + Objects.equals(config, that.config) && + Objects.equals(appliedRules, that.appliedRules); + } + + @Override + public int hashCode() + { + return Objects.hash(interval, config, ruleCount, appliedRules); + } + } +} diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java new file mode 100644 index 000000000000..aacb2751e8f5 --- /dev/null +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java @@ -0,0 +1,81 @@ +/* + * 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.indexing.compact; + +import org.apache.druid.java.util.common.IAE; +import org.apache.druid.java.util.common.granularity.Granularity; +import org.joda.time.Interval; + +/** + * Exception thrown when segment granularity timeline validation fails. + * Contains structured information about the conflicting intervals. + */ +public class GranularityTimelineValidationException extends IAE +{ + private final Interval olderInterval; + private final Granularity olderGranularity; + private final Interval newerInterval; + private final Granularity newerGranularity; + + public GranularityTimelineValidationException( + String dataSource, + Interval olderInterval, + Granularity olderGranularity, + Interval newerInterval, + Granularity newerGranularity + ) + { + super( + "Invalid segment granularity timeline for dataSource[%s]: " + + "Interval[%s] with granularity[%s] is more recent than " + + "interval[%s] with granularity[%s], but has a coarser granularity. " + + "Segment granularity must stay the same or become finer as data ages from present to past.", + dataSource, + newerInterval, + newerGranularity, + olderInterval, + olderGranularity + ); + this.olderInterval = olderInterval; + this.olderGranularity = olderGranularity; + this.newerInterval = newerInterval; + this.newerGranularity = newerGranularity; + } + + public Interval getOlderInterval() + { + return olderInterval; + } + + public Granularity getOlderGranularity() + { + return olderGranularity; + } + + public Interval getNewerInterval() + { + return newerInterval; + } + + public Granularity getNewerGranularity() + { + return newerGranularity; + } +} \ No newline at end of file diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 6b145be07e4a..d8af967db55a 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -35,9 +35,11 @@ import org.apache.commons.lang3.NotImplementedException; import org.apache.druid.audit.AuditEntry; import org.apache.druid.audit.AuditManager; +import org.apache.druid.indexing.compact.CompactionTimelineView; import org.apache.druid.indexing.overlord.DataSourceMetadata; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.http.security.SupervisorResourceFilter; +import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.UOE; import org.apache.druid.segment.incremental.ParseExceptionReport; @@ -51,6 +53,7 @@ import org.apache.druid.server.security.ResourceAction; import org.apache.druid.server.security.ResourceType; import org.apache.druid.utils.CollectionUtils; +import org.joda.time.DateTime; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -351,6 +354,82 @@ public Response getAllTaskStats( ); } + @GET + @Path("/{id}/compactionTimeline") + @Produces(MediaType.APPLICATION_JSON) + @ResourceFilters(SupervisorResourceFilter.class) + public Response getCompactionTimeline( + @PathParam("id") final String id, + @QueryParam("referenceTime") @Nullable final String referenceTimeStr + ) + { + return asLeaderWithSupervisorManager( + manager -> { + Optional specOptional = manager.getSupervisorSpec(id); + if (!specOptional.isPresent()) { + return Response.status(Response.Status.NOT_FOUND) + .entity(ImmutableMap.of("error", StringUtils.format("[%s] does not exist", id))) + .build(); + } + + SupervisorSpec spec = specOptional.get(); + if (!(spec instanceof org.apache.druid.indexing.compact.CompactionSupervisorSpec)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format( + "[%s] is not a compaction supervisor (type: %s)", + id, + spec.getClass().getSimpleName() + ) + )) + .build(); + } + + org.apache.druid.indexing.compact.CompactionSupervisorSpec compactionSpec = + (org.apache.druid.indexing.compact.CompactionSupervisorSpec) spec; + + DateTime referenceTime; + if (referenceTimeStr != null) { + try { + referenceTime = DateTime.parse(referenceTimeStr); + } + catch (IllegalArgumentException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format("Invalid referenceTime format: %s", referenceTimeStr) + )) + .build(); + } + } else { + referenceTime = DateTimes.nowUtc(); + } + + org.apache.druid.indexing.compact.CompactionJobTemplate template = compactionSpec.getTemplate(); + if (!(template instanceof org.apache.druid.indexing.compact.CascadingReindexingTemplate)) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(ImmutableMap.of( + "error", + StringUtils.format( + "Compaction timeline is only available for cascading reindexing supervisors. " + + "Supervisor [%s] uses template type: %s", + id, + template.getClass().getSimpleName() + ) + )) + .build(); + } + + org.apache.druid.indexing.compact.CascadingReindexingTemplate cascadingTemplate = + (org.apache.druid.indexing.compact.CascadingReindexingTemplate) template; + + CompactionTimelineView timelineView = cascadingTemplate.getCompactionTimelineView(referenceTime); + return Response.ok(timelineView).build(); + } + ); + } + @GET @Path("/{id}/parseErrors") @Produces(MediaType.APPLICATION_JSON) diff --git a/web-console/src/components/compaction-timeline/compaction-timeline.scss b/web-console/src/components/compaction-timeline/compaction-timeline.scss new file mode 100644 index 000000000000..87d7651c2ac3 --- /dev/null +++ b/web-console/src/components/compaction-timeline/compaction-timeline.scss @@ -0,0 +1,202 @@ +/* + * 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. + */ + +.compaction-timeline { + padding: 20px; + max-width: 1200px; + + .bp5-callout { + ul { + margin: 10px 0; + padding-left: 20px; + + li { + margin: 8px 0; + line-height: 1.6; + + code { + background-color: rgba(0, 0, 0, 0.1); + padding: 2px 6px; + border-radius: 3px; + font-family: monospace; + font-size: 13px; + } + + .bp5-tag { + margin-left: 5px; + vertical-align: middle; + } + } + } + + p { + margin: 8px 0; + line-height: 1.6; + + &:first-child { + margin-top: 0; + } + + &:last-child { + margin-bottom: 0; + } + } + } + + .timeline-header { + margin-bottom: 20px; + + .header-info { + margin-bottom: 10px; + font-size: 14px; + + .spacer { + margin: 0 10px; + color: #5c7080; + } + + strong { + margin-right: 5px; + } + } + + .skip-offset-info { + display: flex; + gap: 10px; + } + } + + .timeline-bar-container { + margin-bottom: 20px; + padding: 20px; + + .timeline-bar { + display: flex; + gap: 2px; + min-height: 120px; + border-radius: 4px; + overflow: hidden; + + .timeline-segment { + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 10px 5px; + min-width: 60px; + border-radius: 3px; + position: relative; + + &:hover { + opacity: 1 !important; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + } + + &.selected { + box-shadow: 0 0 0 3px rgba(19, 124, 189, 0.6); + transform: translateY(-2px); + } + + .segment-label { + color: white; + text-align: center; + font-size: 11px; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); + + .segment-date { + margin-bottom: 2px; + font-weight: 500; + } + + .segment-rules { + margin-top: 8px; + + .bp5-tag { + background-color: rgba(255, 255, 255, 0.9); + color: #333; + } + } + } + } + } + + .timeline-hint { + margin-top: 10px; + text-align: center; + font-size: 12px; + color: #5c7080; + font-style: italic; + } + } + + .interval-detail-panel { + margin-top: 20px; + padding: 20px; + + .detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 20px; + padding-bottom: 15px; + border-bottom: 1px solid #d3d8de; + + h3 { + margin: 0 0 10px 0; + font-size: 16px; + } + } + + .detail-content { + .config-badges { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 20px; + } + + .full-config-section { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #d3d8de; + display: flex; + gap: 10px; + justify-content: center; + } + } + } +} + +.compaction-config-dialog { + width: 80%; + max-width: 900px; + + .bp5-dialog-body { + margin: 0; + padding: 0; + } + + .config-json-viewer { + .ace_editor { + font-size: 12px; + } + } +} \ No newline at end of file diff --git a/web-console/src/components/compaction-timeline/compaction-timeline.tsx b/web-console/src/components/compaction-timeline/compaction-timeline.tsx new file mode 100644 index 000000000000..1ad1c4a272d8 --- /dev/null +++ b/web-console/src/components/compaction-timeline/compaction-timeline.tsx @@ -0,0 +1,548 @@ +/* + * 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. + */ + +import { Button, Callout, Card, Dialog, Intent, Tag, Tooltip } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as JSONBig from 'json-bigint-native'; +import React, { useState } from 'react'; +import AceEditor from 'react-ace'; + +import { useQueryManager } from '../../hooks'; +import { Api, AppToaster } from '../../singletons'; +import { downloadFile } from '../../utils'; +import { Loader } from '../loader/loader'; + +import './compaction-timeline.scss'; + +interface CompactionTimelineProps { + supervisorId: string; +} + +interface IntervalConfig { + interval: string; + ruleCount: number; + config: any; + appliedRules: any[]; +} + +interface SkipOffsetInfo { + applied?: { + type: string; + period: string; + effectiveEndTime: string; + }; + notApplied?: { + type: string; + period: string; + reason: string; + }; +} + +interface ValidationError { + errorType: string; + message: string; + olderInterval?: string; + olderGranularity?: string; + newerInterval?: string; + newerGranularity?: string; +} + +interface CompactionTimelineData { + dataSource: string; + referenceTime: string; + skipOffset?: SkipOffsetInfo; + intervals: IntervalConfig[]; + validationError?: ValidationError; +} + +export const CompactionTimeline = React.memo(function CompactionTimeline( + props: CompactionTimelineProps, +) { + const { supervisorId } = props; + const [selectedIntervalIndex, setSelectedIntervalIndex] = useState(); + + const [timelineState] = useQueryManager({ + query: supervisorId, + processQuery: async (supervisorId, signal) => { + const resp = await Api.instance.get( + `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/compactionTimeline`, + { signal }, + ); + return resp.data; + }, + }); + + if (timelineState.loading) { + return ; + } + + if (timelineState.error) { + return ( +

    + + {timelineState.getErrorMessage()} + +
    + ); + } + + const timelineData = timelineState.data; + if (!timelineData) { + return null; + } + + const { intervals, skipOffset, referenceTime, validationError } = timelineData; + + // Display validation error if present + if (validationError) { + return ( +
    + +

    + The segment granularity rule definitions have created an illegal segment granularity timeline. +

    + {validationError.errorType === 'INVALID_GRANULARITY_TIMELINE' && validationError.olderInterval && ( + <> +

    + Segment granularity must stay the same or become coarser as data ages from present to past. + Your configuration violates this constraint: +

    +
      +
    • + Older interval (further in the past): {formatInterval(validationError.olderInterval)} + {' '}has finer granularity: {validationError.olderGranularity} +
    • +
    • + Newer interval (more recent): {formatInterval(validationError.newerInterval!)} + {' '}has coarser granularity: {validationError.newerGranularity} +
    • +
    +

    + To fix this: Adjust your segment granularity rules so that as time moves from present to past, + the granularity either stays the same or gets coarser (e.g., HOUR → DAY → MONTH → YEAR). +

    + + )} + {validationError.errorType !== 'INVALID_GRANULARITY_TIMELINE' && ( +

    {validationError.message}

    + )} +
    +
    + ); + } + + if (intervals.length === 0) { + return ( +
    + + No compaction intervals found. This may indicate that the rule provider is not ready or + no rules are configured. + +
    + ); + } + + const selectedInterval = selectedIntervalIndex !== undefined ? intervals[selectedIntervalIndex] : undefined; + + // Generate a color based on interval index using blue/gray scale theme + const getIntervalColor = (index: number) => { + const colors = [ + '#5C7080', // gray + '#738694', // light gray + '#8A9BA8', // lighter gray + '#394B59', // dark gray + '#4A5568', // medium dark gray + '#6B7E91', // medium gray + '#2F343C', // darker gray + ]; + return colors[index % colors.length]; + }; + + return ( +
    +
    +
    + DataSource: {timelineData.dataSource} + | + + + Reference Time: + + {' '} + {new Date(referenceTime).toLocaleString()} +
    + {skipOffset && ( +
    + {skipOffset.applied && ( + + Skip Offset: {skipOffset.applied.type} ({skipOffset.applied.period}) + + )} + {skipOffset.notApplied && ( + + {skipOffset.notApplied.type} not applied: {skipOffset.notApplied.reason} + + )} +
    + )} +
    + + +
    + {intervals.map((interval, idx) => { + const [start, end] = interval.interval.split('/'); + const isSelected = selectedIntervalIndex === idx; + return ( +
    setSelectedIntervalIndex(idx)} + title={`${interval.interval}\n${interval.ruleCount} rule(s) applied`} + > +
    +
    {formatDateShort(start)}
    +
    {formatDateShort(end)}
    +
    + + {interval.ruleCount} rule{interval.ruleCount !== 1 ? 's' : ''} + +
    +
    +
    + ); + })} +
    +
    + Click on an interval to view its configuration details +
    +
    + + {selectedInterval && ( + setSelectedIntervalIndex(undefined)} + /> + )} +
    + ); +}); + +function formatDateShort(isoDate: string): string { + // Handle start of time / very old dates + if (isoDate.startsWith('-')) { + return '-INF'; + } + + const date = new Date(isoDate); + + const month = date.toLocaleString('default', { month: 'short' }); + const day = date.getDate(); + const year = date.getFullYear(); + return `${month} ${day}, ${year}`; +} + +function formatInterval(interval: string): string { + const [start, end] = interval.split('/'); + const formattedStart = start.startsWith('-') ? '-INF' : start; + const formattedEnd = end ? end : 'now'; + return `${formattedStart}/${formattedEnd}`; +} + +interface IntervalDetailPanelProps { + interval: IntervalConfig; + onClose: () => void; +} + +function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { + const [showFullConfig, setShowFullConfig] = useState(false); + const [showRawRules, setShowRawRules] = useState(false); + const { config } = interval; + + // Count deletion rules from transform spec + const deletionRuleCount = countDeletionRules(config.transformSpec); + const metricsCount = config.metricsSpec?.length || 0; + const dimensionsCount = config.dimensionsSpec?.dimensions?.length || 0; + const projectionsCount = config.projections?.length || 0; + + return ( + <> + +
    +
    +

    Interval: {formatInterval(interval.interval)}

    + + {interval.ruleCount} rule{interval.ruleCount !== 1 ? 's' : ''} applied + +
    +
    + +
    +
    + {config.granularitySpec?.segmentGranularity && ( + + Segment: {config.granularitySpec.segmentGranularity} + + )} + {config.granularitySpec?.queryGranularity && ( + + Query: {config.granularitySpec.queryGranularity} + + )} + {metricsCount > 0 && ( + + {metricsCount} metric{metricsCount !== 1 ? 's' : ''} + + )} + {dimensionsCount > 0 && ( + + {dimensionsCount} dimension{dimensionsCount !== 1 ? 's' : ''} + + )} + {projectionsCount > 0 && ( + + {projectionsCount} projection{projectionsCount !== 1 ? 's' : ''} + + )} + {deletionRuleCount > 0 && ( + + {deletionRuleCount} deletion rule{deletionRuleCount !== 1 ? 's' : ''} + + )} +
    + +
    +
    +
    +
    + + setShowFullConfig(false)} + title={ + + {formatInterval(interval.interval)} + + } + className="compaction-config-dialog" + canOutsideClickClose + > +
    + +
    +
    +
    +
    +
    +
    + + setShowRawRules(false)} + title={ + + Rules for {formatInterval(interval.interval)} + + } + className="compaction-config-dialog" + canOutsideClickClose + > +
    + +
    +
    +
    +
    +
    +
    + + ); +} + +function countDeletionRules(transformSpec: any): number { + if (!transformSpec || !transformSpec.filter) { + return 0; + } + + const filter = transformSpec.filter; + + // Check if it's a NotDimFilter with fields + if (filter.type === 'not' && filter.field) { + // If the field is an 'or' filter, count the number of fields in it + if (filter.field.type === 'or' && filter.field.fields) { + return filter.field.fields.length; + } + // Otherwise it's a single deletion rule + return 1; + } + + return 0; +} + +interface ConfigJsonViewerProps { + config: any; + isRulesList?: boolean; +} + +function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { + let displayValue: any; + let jsonValue: string; + + if (isRulesList && Array.isArray(config)) { + // Group rules by type for better readability + const rulesByType: Record = {}; + config.forEach((rule: any) => { + const type = getRuleTypeName(rule); + if (!rulesByType[type]) { + rulesByType[type] = []; + } + rulesByType[type].push(rule); + }); + displayValue = rulesByType; + jsonValue = JSONBig.stringify(displayValue, undefined, 2); + } else { + displayValue = config; + jsonValue = JSONBig.stringify(config, undefined, 2); + } + + return ( +
    + +
    + ); +} + +function getRuleTypeName(rule: any): string { + // Infer rule type from its properties since there's no explicit type field + + // DeletionRule has a required 'deleteWhere' field + if (rule.deleteWhere) { + return 'Deletion Rules'; + } + + // DataSchemaRule has these fields + if (rule.dimensionsSpec || rule.metricsSpec || rule.projections || rule.queryGranularity !== undefined) { + return 'Data Schema Rules'; + } + + // SegmentGranularityRule has segmentGranularity + if (rule.segmentGranularity) { + return 'Segment Granularity Rules'; + } + + // TuningConfigRule has tuningConfig + if (rule.tuningConfig) { + return 'Tuning Config Rules'; + } + + // IOConfigRule has ioConfig + if (rule.ioConfig) { + return 'IO Config Rules'; + } + + // Fallback: use the id to infer type if it contains type info + const id = rule.id || ''; + if (id.toLowerCase().includes('delete')) return 'Deletion Rules'; + if (id.toLowerCase().includes('dataschema')) return 'Data Schema Rules'; + if (id.toLowerCase().includes('granularity')) return 'Segment Granularity Rules'; + if (id.toLowerCase().includes('tuning')) return 'Tuning Config Rules'; + if (id.toLowerCase().includes('io')) return 'IO Config Rules'; + + return 'Other Rules'; +} diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts index 849cc470ccf5..294ce9475aa0 100644 --- a/web-console/src/components/index.ts +++ b/web-console/src/components/index.ts @@ -25,6 +25,7 @@ export * from './braced-text/braced-text'; export * from './center-message/center-message'; export * from './clearable-input/clearable-input'; export * from './click-to-copy/click-to-copy'; +export * from './compaction-timeline/compaction-timeline'; export * from './deferred/deferred'; export * from './druid-logo/druid-logo'; export * from './external-link/external-link'; diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx index 44ef05d5d59f..069fb5f7a318 100644 --- a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx +++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx @@ -18,7 +18,7 @@ import React, { useState } from 'react'; -import { ShowJson, SupervisorHistoryPanel } from '../../components'; +import { CompactionTimeline, ShowJson, SupervisorHistoryPanel } from '../../components'; import { cleanSpec } from '../../druid-models'; import { Api } from '../../singletons'; import { deepGet } from '../../utils'; @@ -28,7 +28,7 @@ import { TableActionDialog } from '../table-action-dialog/table-action-dialog'; import { SupervisorStatisticsTable } from './supervisor-statistics-table/supervisor-statistics-table'; -type SupervisorTableActionDialogTab = 'status' | 'stats' | 'spec' | 'history'; +type SupervisorTableActionDialogTab = 'status' | 'stats' | 'spec' | 'history' | 'timeline'; interface SupervisorTableActionDialogProps { supervisorId: string; @@ -67,6 +67,12 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc active: activeTab === 'history', onClick: () => setActiveTab('history'), }, + { + icon: 'timeline-events', + text: 'Timeline', + active: activeTab === 'timeline', + onClick: () => setActiveTab('timeline'), + }, ]; const supervisorEndpointBase = `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}`; @@ -98,6 +104,7 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc /> )} {activeTab === 'history' && } + {activeTab === 'timeline' && } ); }); From ebf40cfb9332e83e1ec24e3cbf78844f9cb0ae0a Mon Sep 17 00:00:00 2001 From: capistrant Date: Sat, 14 Feb 2026 11:25:47 -0600 Subject: [PATCH 54/90] naming refactor --- .../compact/CascadingReindexingTemplate.java | 79 +++------- .../compact/CompactionSupervisor.java | 4 +- ...eView.java => ReindexingTimelineView.java} | 12 +- .../supervisor/SupervisorResource.java | 25 +-- .../compaction/ReindexingConfigBuilder.java | 148 +++++++++++------- .../ReindexingConfigBuilderTest.java | 46 ++++++ web-console/src/components/index.ts | 2 +- .../reindexing-timeline.scss} | 4 +- .../reindexing-timeline.tsx} | 36 ++--- .../supervisor-table-action-dialog.tsx | 4 +- 10 files changed, 204 insertions(+), 156 deletions(-) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{CompactionTimelineView.java => ReindexingTimelineView.java} (97%) rename web-console/src/components/{compaction-timeline/compaction-timeline.scss => reindexing-timeline/reindexing-timeline.scss} (98%) rename web-console/src/components/{compaction-timeline/compaction-timeline.tsx => reindexing-timeline/reindexing-timeline.tsx} (95%) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 503f4bec53aa..63c265f94fa8 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -37,13 +37,9 @@ import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.compaction.ReindexingConfigBuilder; -import org.apache.druid.server.compaction.ReindexingDataSchemaRule; -import org.apache.druid.server.compaction.ReindexingDeletionRule; -import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; -import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -277,14 +273,14 @@ private static boolean shouldOptimizeFilterRules( } /** - * Generates a timeline view showing the search intervals and their associated compaction + * Generates a timeline view showing the search intervals and their associated reindexing * configurations. This is useful for operators to understand how rules are applied across - * different time periods without actually creating compaction jobs. + * different time periods and to preview the effects of rule changes before they are applied. * * @param referenceTime the reference time to use for computing rule periods (typically DateTime.now()) - * @return a view of the compaction timeline with intervals and their configs + * @return a view of the reindexing timeline with intervals and their configs */ - public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) + public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) { if (!ruleProvider.isReady()) { LOG.info( @@ -292,7 +288,7 @@ public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) ruleProvider.getType(), dataSource ); - return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); + return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); } List searchIntervals; @@ -301,8 +297,8 @@ public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) } catch (GranularityTimelineValidationException e) { // Validation failed - extract structured error details and return with validation error - LOG.warn(e, "Validation failed for compaction timeline of dataSource[%s]", dataSource); - CompactionTimelineView.ValidationError validationError = new CompactionTimelineView.ValidationError( + LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); + ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( "INVALID_GRANULARITY_TIMELINE", e.getMessage(), e.getOlderInterval().toString(), @@ -310,12 +306,12 @@ public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) e.getNewerInterval().toString(), e.getNewerGranularity().toString() ); - return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); + return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); } catch (IAE e) { // Other validation errors (e.g., no rules configured) - LOG.warn(e, "Validation failed for compaction timeline of dataSource[%s]", dataSource); - CompactionTimelineView.ValidationError validationError = new CompactionTimelineView.ValidationError( + LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); + ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( "VALIDATION_ERROR", e.getMessage(), null, @@ -323,38 +319,38 @@ public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) null, null ); - return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); + return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); } if (searchIntervals.isEmpty()) { LOG.warn("No search intervals generated for dataSource[%s]", dataSource); - return new CompactionTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); + return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); } // Calculate effective end time based on skip offset DateTime effectiveEndTime = referenceTime; - CompactionTimelineView.SkipOffsetInfo skipOffsetInfo = null; + ReindexingTimelineView.SkipOffsetInfo skipOffsetInfo = null; if (skipOffsetFromNow != null) { effectiveEndTime = referenceTime.minus(skipOffsetFromNow); - CompactionTimelineView.AppliedSkipOffset applied = new CompactionTimelineView.AppliedSkipOffset( + ReindexingTimelineView.AppliedSkipOffset applied = new ReindexingTimelineView.AppliedSkipOffset( "skipOffsetFromNow", skipOffsetFromNow, effectiveEndTime ); - skipOffsetInfo = new CompactionTimelineView.SkipOffsetInfo(applied, null); + skipOffsetInfo = new ReindexingTimelineView.SkipOffsetInfo(applied, null); } else if (skipOffsetFromLatest != null) { // skipOffsetFromLatest requires actual timeline data, so we can't apply it in preview mode - CompactionTimelineView.NotAppliedSkipOffset notApplied = new CompactionTimelineView.NotAppliedSkipOffset( + ReindexingTimelineView.NotAppliedSkipOffset notApplied = new ReindexingTimelineView.NotAppliedSkipOffset( "skipOffsetFromLatest", skipOffsetFromLatest, "Requires actual segment timeline data" ); - skipOffsetInfo = new CompactionTimelineView.SkipOffsetInfo(null, notApplied); + skipOffsetInfo = new ReindexingTimelineView.SkipOffsetInfo(null, notApplied); } // Build configs for each interval - List intervalConfigs = new ArrayList<>(); + List intervalConfigs = new ArrayList<>(); for (Interval searchInterval : searchIntervals) { // Clamp interval to effective end time Interval clampedInterval = searchInterval; @@ -374,46 +370,19 @@ public CompactionTimelineView getCompactionTimelineView(DateTime referenceTime) clampedInterval, referenceTime ); - int ruleCount = configBuilder.applyTo(builder); - - if (ruleCount > 0) { - // Collect the ACTUAL rules that were applied (matching ReindexingConfigBuilder logic) - List appliedRules = new ArrayList<>(); - - ReindexingTuningConfigRule tuningRule = ruleProvider.getTuningConfigRule(clampedInterval, referenceTime); - if (tuningRule != null) { - appliedRules.add(tuningRule); - } - - ReindexingIOConfigRule ioConfigRule = ruleProvider.getIOConfigRule(clampedInterval, referenceTime); - if (ioConfigRule != null) { - appliedRules.add(ioConfigRule); - } - - ReindexingDataSchemaRule dataSchemaRule = ruleProvider.getDataSchemaRule(clampedInterval, referenceTime); - if (dataSchemaRule != null) { - appliedRules.add(dataSchemaRule); - } - - // Deletion rules are additive - collect all that apply - List deletionRules = ruleProvider.getDeletionRules(clampedInterval, referenceTime); - appliedRules.addAll(deletionRules); - - ReindexingSegmentGranularityRule segmentGranularityRule = ruleProvider.getSegmentGranularityRule(clampedInterval, referenceTime); - if (segmentGranularityRule != null) { - appliedRules.add(segmentGranularityRule); - } + ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builder); - intervalConfigs.add(new CompactionTimelineView.IntervalConfig( + if (buildResult.getRuleCount() > 0) { + intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( clampedInterval, - ruleCount, + buildResult.getRuleCount(), builder.build(), - appliedRules + buildResult.getAppliedRules() )); } } - return new CompactionTimelineView(dataSource, referenceTime, skipOffsetInfo, intervalConfigs, null); + return new ReindexingTimelineView(dataSource, referenceTime, skipOffsetInfo, intervalConfigs, null); } @Override diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java index 7f85573c0260..dab32ea45d4a 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java @@ -85,11 +85,11 @@ public List createJobs( * @param referenceTime the reference time to use for computing rule periods * @return Optional containing the timeline view if this is a cascading reindexing supervisor, empty otherwise */ - public java.util.Optional getCompactionTimelineView(org.joda.time.DateTime referenceTime) + public java.util.Optional getCompactionTimelineView(org.joda.time.DateTime referenceTime) { CompactionJobTemplate template = supervisorSpec.getTemplate(); if (template instanceof CascadingReindexingTemplate) { - return java.util.Optional.of(((CascadingReindexingTemplate) template).getCompactionTimelineView(referenceTime)); + return java.util.Optional.of(((CascadingReindexingTemplate) template).getReindexingTimelineView(referenceTime)); } return java.util.Optional.empty(); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java similarity index 97% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java index 4d242ea78012..b143b968298b 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionTimelineView.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java @@ -33,11 +33,11 @@ import java.util.Objects; /** - * Represents the timeline of search intervals and their associated compaction configurations + * Represents the timeline of search intervals and their associated reindexing configurations * for a cascading reindexing supervisor. This view helps operators understand how different - * compaction rules are applied across time intervals. + * rules are applied across time intervals. */ -public class CompactionTimelineView +public class ReindexingTimelineView { private final String dataSource; private final DateTime referenceTime; @@ -46,7 +46,7 @@ public class CompactionTimelineView private final ValidationError validationError; @JsonCreator - public CompactionTimelineView( + public ReindexingTimelineView( @JsonProperty("dataSource") String dataSource, @JsonProperty("referenceTime") DateTime referenceTime, @JsonProperty("skipOffset") @Nullable SkipOffsetInfo skipOffset, @@ -104,7 +104,7 @@ public boolean equals(Object o) if (o == null || getClass() != o.getClass()) { return false; } - CompactionTimelineView that = (CompactionTimelineView) o; + ReindexingTimelineView that = (ReindexingTimelineView) o; return Objects.equals(dataSource, that.dataSource) && Objects.equals(referenceTime, that.referenceTime) && Objects.equals(skipOffset, that.skipOffset) && @@ -395,7 +395,7 @@ public int hashCode() } /** - * Represents a search interval and its associated compaction configuration. + * Represents a search interval and its associated reindexing configuration. */ public static class IntervalConfig { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index d8af967db55a..8c68e5891bae 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -35,7 +35,10 @@ import org.apache.commons.lang3.NotImplementedException; import org.apache.druid.audit.AuditEntry; import org.apache.druid.audit.AuditManager; -import org.apache.druid.indexing.compact.CompactionTimelineView; +import org.apache.druid.indexing.compact.CascadingReindexingTemplate; +import org.apache.druid.indexing.compact.CompactionJobTemplate; +import org.apache.druid.indexing.compact.CompactionSupervisorSpec; +import org.apache.druid.indexing.compact.ReindexingTimelineView; import org.apache.druid.indexing.overlord.DataSourceMetadata; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.http.security.SupervisorResourceFilter; @@ -355,10 +358,10 @@ public Response getAllTaskStats( } @GET - @Path("/{id}/compactionTimeline") + @Path("/{id}/reindexingTimeline") @Produces(MediaType.APPLICATION_JSON) @ResourceFilters(SupervisorResourceFilter.class) - public Response getCompactionTimeline( + public Response getReindexingTimeline( @PathParam("id") final String id, @QueryParam("referenceTime") @Nullable final String referenceTimeStr ) @@ -373,7 +376,7 @@ public Response getCompactionTimeline( } SupervisorSpec spec = specOptional.get(); - if (!(spec instanceof org.apache.druid.indexing.compact.CompactionSupervisorSpec)) { + if (!(spec instanceof CompactionSupervisorSpec)) { return Response.status(Response.Status.BAD_REQUEST) .entity(ImmutableMap.of( "error", @@ -386,8 +389,7 @@ public Response getCompactionTimeline( .build(); } - org.apache.druid.indexing.compact.CompactionSupervisorSpec compactionSpec = - (org.apache.druid.indexing.compact.CompactionSupervisorSpec) spec; + CompactionSupervisorSpec compactionSpec = (CompactionSupervisorSpec) spec; DateTime referenceTime; if (referenceTimeStr != null) { @@ -406,13 +408,13 @@ public Response getCompactionTimeline( referenceTime = DateTimes.nowUtc(); } - org.apache.druid.indexing.compact.CompactionJobTemplate template = compactionSpec.getTemplate(); - if (!(template instanceof org.apache.druid.indexing.compact.CascadingReindexingTemplate)) { + CompactionJobTemplate template = compactionSpec.getTemplate(); + if (!(template instanceof CascadingReindexingTemplate)) { return Response.status(Response.Status.BAD_REQUEST) .entity(ImmutableMap.of( "error", StringUtils.format( - "Compaction timeline is only available for cascading reindexing supervisors. " + + "Reindexing timeline is only available for cascading reindexing supervisors. " + "Supervisor [%s] uses template type: %s", id, template.getClass().getSimpleName() @@ -421,10 +423,9 @@ public Response getCompactionTimeline( .build(); } - org.apache.druid.indexing.compact.CascadingReindexingTemplate cascadingTemplate = - (org.apache.druid.indexing.compact.CascadingReindexingTemplate) template; + CascadingReindexingTemplate cascadingTemplate = (CascadingReindexingTemplate) template; - CompactionTimelineView timelineView = cascadingTemplate.getCompactionTimelineView(referenceTime); + ReindexingTimelineView timelineView = cascadingTemplate.getReindexingTimelineView(referenceTime); return Response.ok(timelineView).build(); } ); diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index 77cb4968ac2f..895e20e9b260 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -51,6 +51,38 @@ public class ReindexingConfigBuilder private final Interval interval; private final DateTime referenceTime; + /** + * Result of applying reindexing rules to a config builder. + * Contains both the count of rules applied and the actual rules that were applied. + */ + public static class BuildResult + { + private final int ruleCount; + private final List appliedRules; + + public BuildResult(int ruleCount, List appliedRules) + { + this.ruleCount = ruleCount; + this.appliedRules = appliedRules; + } + + /** + * @return the number of rules that were applied + */ + public int getRuleCount() + { + return ruleCount; + } + + /** + * @return immutable list of the actual rules that were applied, in application order + */ + public List getAppliedRules() + { + return appliedRules; + } + } + public ReindexingConfigBuilder( ReindexingRuleProvider provider, Granularity defaultSegmentGranularity, Interval interval, @@ -69,25 +101,54 @@ public ReindexingConfigBuilder( * @return number of rules applied */ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) + { + return applyToWithDetails(builder).getRuleCount(); + } + + /** + * Applies all applicable rules to the builder and returns detailed information about + * which rules were applied. + * + * @return BuildResult containing the count and list of applied rules + */ + public BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Builder builder) { int count = 0; + List appliedRules = new ArrayList<>(); - count += applyIfPresent( - builder::withTuningConfig, - provider.getTuningConfigRule(interval, referenceTime), - ReindexingTuningConfigRule::getTuningConfig - ); + // Apply tuning config rule + ReindexingTuningConfigRule tuningRule = provider.getTuningConfigRule(interval, referenceTime); + if (tuningRule != null) { + builder.withTuningConfig(tuningRule.getTuningConfig()); + appliedRules.add(tuningRule); + count++; + } - count += applyIfPresent( - builder::withIoConfig, - provider.getIOConfigRule(interval, referenceTime), - ReindexingIOConfigRule::getIoConfig - ); + // Apply IO config rule + ReindexingIOConfigRule ioConfigRule = provider.getIOConfigRule(interval, referenceTime); + if (ioConfigRule != null) { + builder.withIoConfig(ioConfigRule.getIoConfig()); + appliedRules.add(ioConfigRule); + count++; + } - count += applyDataSchemaRules(builder); + // Apply data schema rules + ReindexingDataSchemaRule dataSchemaRule = provider.getDataSchemaRule(interval, referenceTime); + if (dataSchemaRule != null) { + applyDataSchemaRule(builder, dataSchemaRule); + appliedRules.add(dataSchemaRule); + count++; + } - count += applyDeletionRules(builder); + // Apply deletion rules (additive) + List deletionRules = provider.getDeletionRules(interval, referenceTime); + if (!deletionRules.isEmpty()) { + applyDeletionRulesList(builder, deletionRules); + appliedRules.addAll(deletionRules); + count += deletionRules.size(); + } + // Apply segment granularity rule ReindexingSegmentGranularityRule segmentGranularityRule = provider.getSegmentGranularityRule(interval, referenceTime); if (segmentGranularityRule == null) { if (count > 0) { @@ -95,55 +156,30 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) builder.withSegmentGranularity(defaultGranularity); } } else { - count++; builder.withSegmentGranularity(segmentGranularityRule.getSegmentGranularity()); + appliedRules.add(segmentGranularityRule); + count++; } - return count; + return new BuildResult(count, appliedRules); } - // Generic helper for non-additive rules - private int applyIfPresent( - Consumer setter, - @Nullable R rule, - Function configExtractor + private void applyDataSchemaRule( + InlineSchemaDataSourceCompactionConfig.Builder builder, + ReindexingDataSchemaRule dataSchemaRule ) { - if (rule != null) { - C config = configExtractor.apply(rule); - setter.accept(config); - return 1; + if (dataSchemaRule.getDimensionsSpec() != null) { + builder.withDimensionsSpec(dataSchemaRule.getDimensionsSpec()); } - return 0; - } - private int applyDataSchemaRules(InlineSchemaDataSourceCompactionConfig.Builder builder) - { - ReindexingDataSchemaRule dataSchemaRule = provider.getDataSchemaRule( - interval, - referenceTime - ); - if (dataSchemaRule == null) { - return 0; + if (dataSchemaRule.getMetricsSpec() != null) { + builder.withMetricsSpec(dataSchemaRule.getMetricsSpec()); } - applyIfPresent( - builder::withDimensionsSpec, - dataSchemaRule, - ReindexingDataSchemaRule::getDimensionsSpec - ); - - applyIfPresent( - builder::withMetricsSpec, - dataSchemaRule, - ReindexingDataSchemaRule::getMetricsSpec - ); - - applyIfPresent( - builder::withProjections, - dataSchemaRule, - ReindexingDataSchemaRule::getProjections - ); + if (dataSchemaRule.getProjections() != null) { + builder.withProjections(dataSchemaRule.getProjections()); + } if (dataSchemaRule.getQueryGranularity() != null || dataSchemaRule.getRollup() != null) { builder.withQueryGranularityAndRollup( @@ -151,16 +187,13 @@ private int applyDataSchemaRules(InlineSchemaDataSourceCompactionConfig.Builder dataSchemaRule.getRollup() ); } - - return 1; } - private int applyDeletionRules(InlineSchemaDataSourceCompactionConfig.Builder builder) + private void applyDeletionRulesList( + InlineSchemaDataSourceCompactionConfig.Builder builder, + List rules + ) { - List rules = provider.getDeletionRules(interval, referenceTime); - if (rules.isEmpty()) { - return 0; - } // Collect filters and virtual columns in a single pass List removeConditions = new ArrayList<>(); @@ -188,6 +221,5 @@ private int applyDeletionRules(InlineSchemaDataSourceCompactionConfig.Builder bu builder.withTransformSpec(new CompactionTransformSpec(finalFilter, virtualColumns)); LOG.debug("Applied [%d] filter rules for interval %s", rules.size(), interval); - return rules.size(); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index ccc6f3879c10..2aaa2c1be45d 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -87,6 +87,25 @@ public void test_applyTo_handlesSynteticSegmentGranularityInsertion() Assert.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); Assert.assertNotNull(config.getGranularitySpec().isRollup()); Assert.assertTrue(config.getGranularitySpec().isRollup()); + + // Test applyToWithDetails() on a fresh builder + InlineSchemaDataSourceCompactionConfig.Builder builderForDetails = + InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource("test_datasource"); + + ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builderForDetails); + + // Verify count matches + Assert.assertEquals(count, buildResult.getRuleCount()); + + // Verify applied rules - should only contain the data schema rule, not the synthetic segment granularity + Assert.assertNotNull(buildResult.getAppliedRules()); + Assert.assertEquals(1, buildResult.getAppliedRules().size()); + Assert.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingDataSchemaRule); + + // Verify config matches + InlineSchemaDataSourceCompactionConfig configFromDetails = builderForDetails.build(); + Assert.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); } @Test @@ -137,6 +156,33 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount OrDimFilter orFilter = (OrDimFilter) notFilter.getField(); Assert.assertEquals(2, orFilter.getFields().size()); // 2 filters combined + + // Now test applyToWithDetails() on a fresh builder + InlineSchemaDataSourceCompactionConfig.Builder builderForDetails = + InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource("test_datasource"); + + ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builderForDetails); + + // Verify BuildResult count matches applyTo() count + Assert.assertEquals(count, buildResult.getRuleCount()); + + // Verify applied rules list + Assert.assertNotNull(buildResult.getAppliedRules()); + Assert.assertEquals(6, buildResult.getAppliedRules().size()); + + // Verify rule types in order: tuning, io, dataSchema, 2 deletion rules, segment granularity + Assert.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingTuningConfigRule); + Assert.assertTrue(buildResult.getAppliedRules().get(1) instanceof ReindexingIOConfigRule); + Assert.assertTrue(buildResult.getAppliedRules().get(2) instanceof ReindexingDataSchemaRule); + Assert.assertTrue(buildResult.getAppliedRules().get(3) instanceof ReindexingDeletionRule); + Assert.assertTrue(buildResult.getAppliedRules().get(4) instanceof ReindexingDeletionRule); + Assert.assertTrue(buildResult.getAppliedRules().get(5) instanceof ReindexingSegmentGranularityRule); + + // Verify the config produced by applyToWithDetails() matches the original + InlineSchemaDataSourceCompactionConfig configFromDetails = builderForDetails.build(); + Assert.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); + Assert.assertEquals(config.getTuningConfig(), configFromDetails.getTuningConfig()); } @Test diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts index 294ce9475aa0..75fd22a5b3a2 100644 --- a/web-console/src/components/index.ts +++ b/web-console/src/components/index.ts @@ -25,7 +25,7 @@ export * from './braced-text/braced-text'; export * from './center-message/center-message'; export * from './clearable-input/clearable-input'; export * from './click-to-copy/click-to-copy'; -export * from './compaction-timeline/compaction-timeline'; +export * from './reindexing-timeline/reindexing-timeline'; export * from './deferred/deferred'; export * from './druid-logo/druid-logo'; export * from './external-link/external-link'; diff --git a/web-console/src/components/compaction-timeline/compaction-timeline.scss b/web-console/src/components/reindexing-timeline/reindexing-timeline.scss similarity index 98% rename from web-console/src/components/compaction-timeline/compaction-timeline.scss rename to web-console/src/components/reindexing-timeline/reindexing-timeline.scss index 87d7651c2ac3..9581d0173880 100644 --- a/web-console/src/components/compaction-timeline/compaction-timeline.scss +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.scss @@ -16,7 +16,7 @@ * limitations under the License. */ -.compaction-timeline { +.reindexing-timeline { padding: 20px; max-width: 1200px; @@ -185,7 +185,7 @@ } } -.compaction-config-dialog { +.reindexing-config-dialog { width: 80%; max-width: 900px; diff --git a/web-console/src/components/compaction-timeline/compaction-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx similarity index 95% rename from web-console/src/components/compaction-timeline/compaction-timeline.tsx rename to web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index 1ad1c4a272d8..4f38dfb13004 100644 --- a/web-console/src/components/compaction-timeline/compaction-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -27,9 +27,9 @@ import { Api, AppToaster } from '../../singletons'; import { downloadFile } from '../../utils'; import { Loader } from '../loader/loader'; -import './compaction-timeline.scss'; +import './reindexing-timeline.scss'; -interface CompactionTimelineProps { +interface ReindexingTimelineProps { supervisorId: string; } @@ -62,7 +62,7 @@ interface ValidationError { newerGranularity?: string; } -interface CompactionTimelineData { +interface ReindexingTimelineData { dataSource: string; referenceTime: string; skipOffset?: SkipOffsetInfo; @@ -70,17 +70,17 @@ interface CompactionTimelineData { validationError?: ValidationError; } -export const CompactionTimeline = React.memo(function CompactionTimeline( - props: CompactionTimelineProps, +export const ReindexingTimeline = React.memo(function ReindexingTimeline( + props: ReindexingTimelineProps, ) { const { supervisorId } = props; const [selectedIntervalIndex, setSelectedIntervalIndex] = useState(); - const [timelineState] = useQueryManager({ + const [timelineState] = useQueryManager({ query: supervisorId, processQuery: async (supervisorId, signal) => { - const resp = await Api.instance.get( - `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/compactionTimeline`, + const resp = await Api.instance.get( + `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/reindexingTimeline`, { signal }, ); return resp.data; @@ -93,8 +93,8 @@ export const CompactionTimeline = React.memo(function CompactionTimeline( if (timelineState.error) { return ( -
    - +
    + {timelineState.getErrorMessage()}
    @@ -111,7 +111,7 @@ export const CompactionTimeline = React.memo(function CompactionTimeline( // Display validation error if present if (validationError) { return ( -
    +

    The segment granularity rule definitions have created an illegal segment granularity timeline. @@ -148,9 +148,9 @@ export const CompactionTimeline = React.memo(function CompactionTimeline( if (intervals.length === 0) { return ( -

    +
    - No compaction intervals found. This may indicate that the rule provider is not ready or + No reindexing intervals found. This may indicate that the rule provider is not ready or no rules are configured.
    @@ -174,7 +174,7 @@ export const CompactionTimeline = React.memo(function CompactionTimeline( }; return ( -
    +
    DataSource: {timelineData.dataSource} @@ -359,7 +359,7 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { {formatInterval(interval.interval)} } - className="compaction-config-dialog" + className="reindexing-config-dialog" canOutsideClickClose >
    @@ -386,7 +386,7 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { intent={Intent.PRIMARY} onClick={() => { const jsonValue = JSONBig.stringify(config, undefined, 2); - const downloadFilename = `compaction-config-${interval.interval.replace(/\//g, '-')}.json`; + const downloadFilename = `reindexing-config-${interval.interval.replace(/\//g, '-')}.json`; downloadFile(jsonValue, 'json', downloadFilename); }} /> @@ -402,7 +402,7 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { Rules for {formatInterval(interval.interval)} } - className="compaction-config-dialog" + className="reindexing-config-dialog" canOutsideClickClose >
    @@ -429,7 +429,7 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { intent={Intent.PRIMARY} onClick={() => { const jsonValue = JSONBig.stringify(interval.appliedRules, undefined, 2); - const downloadFilename = `compaction-rules-${interval.interval.replace(/\//g, '-')}.json`; + const downloadFilename = `reindexing-rules-${interval.interval.replace(/\//g, '-')}.json`; downloadFile(jsonValue, 'json', downloadFilename); }} /> diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx index 069fb5f7a318..dcac7cbef306 100644 --- a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx +++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx @@ -18,7 +18,7 @@ import React, { useState } from 'react'; -import { CompactionTimeline, ShowJson, SupervisorHistoryPanel } from '../../components'; +import { ReindexingTimeline, ShowJson, SupervisorHistoryPanel } from '../../components'; import { cleanSpec } from '../../druid-models'; import { Api } from '../../singletons'; import { deepGet } from '../../utils'; @@ -104,7 +104,7 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc /> )} {activeTab === 'history' && } - {activeTab === 'timeline' && } + {activeTab === 'timeline' && } ); }); From d510cb46a5cdac8c561515fa0eb7d6979691464c Mon Sep 17 00:00:00 2001 From: capistrant Date: Sat, 14 Feb 2026 12:25:56 -0600 Subject: [PATCH 55/90] Refactors and UTs --- .../compact/CompactionSupervisor.java | 16 - ...ranularityTimelineValidationException.java | 4 +- .../compact/ReindexingTimelineView.java | 15 + .../CascadingReindexingTemplateTest.java | 324 ++++++++++++++++++ .../reindexing-timeline.tsx | 44 +-- 5 files changed, 350 insertions(+), 53 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java index dab32ea45d4a..f9ba0eee6db7 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionSupervisor.java @@ -78,22 +78,6 @@ public List createJobs( return supervisorSpec.getTemplate().createCompactionJobs(inputSource, jobParams); } - /** - * Gets a timeline view of the compaction intervals and their associated configurations. - * This is only supported for CascadingReindexingTemplate-based supervisors. - * - * @param referenceTime the reference time to use for computing rule periods - * @return Optional containing the timeline view if this is a cascading reindexing supervisor, empty otherwise - */ - public java.util.Optional getCompactionTimelineView(org.joda.time.DateTime referenceTime) - { - CompactionJobTemplate template = supervisorSpec.getTemplate(); - if (template instanceof CascadingReindexingTemplate) { - return java.util.Optional.of(((CascadingReindexingTemplate) template).getReindexingTimelineView(referenceTime)); - } - return java.util.Optional.empty(); - } - @Override public void start() { diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java index aacb2751e8f5..ffbf086ad4d2 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java @@ -24,7 +24,7 @@ import org.joda.time.Interval; /** - * Exception thrown when segment granularity timeline validation fails. + * Exception thrown when segment granularity timeline validation fails when using {@link CascadingReindexingTemplate}. * Contains structured information about the conflicting intervals. */ public class GranularityTimelineValidationException extends IAE @@ -78,4 +78,4 @@ public Granularity getNewerGranularity() { return newerGranularity; } -} \ No newline at end of file +} diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java index b143b968298b..a6f79dbb027c 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java @@ -22,7 +22,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.druid.server.compaction.ReindexingDataSchemaRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingRule; +import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -437,6 +444,14 @@ public DataSourceCompactionConfig getConfig() } @JsonProperty + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = ReindexingDataSchemaRule.class, name = "dataSchema"), + @JsonSubTypes.Type(value = ReindexingDeletionRule.class, name = "deletion"), + @JsonSubTypes.Type(value = ReindexingSegmentGranularityRule.class, name = "segmentGranularity"), + @JsonSubTypes.Type(value = ReindexingTuningConfigRule.class, name = "tuningConfig"), + @JsonSubTypes.Type(value = ReindexingIOConfigRule.class, name = "ioConfig") + }) public List getAppliedRules() { return appliedRules; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 402dc86d5fd6..bbed0ce3d2e2 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -29,13 +29,22 @@ import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.filter.EqualityFilter; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingDataSchemaRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; +import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.testing.InitializedNullHandlingTest; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentTimeline; @@ -1302,6 +1311,321 @@ public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranul ); } + /** + * Comprehensive test covering: + * - Multiple intervals with different segment granularities + * - All rule types (segment gran, data schema, deletion, tuning, IO) + * - Non-segment-gran rules triggering interval splitting + * - Applied rules tracking with correct rule types in each interval + * - Full DataSourceCompactionConfig generation + * - Rule count accuracy + */ + @Test + public void test_getReindexingTimelineView_comprehensive() + { + DateTime referenceTime = DateTimes.of("2025-02-01T00:00:00Z"); + + // Create rules with various periods to test interval generation and splitting + ReindexingSegmentGranularityRule segGran7d = new ReindexingSegmentGranularityRule( + "seg-gran-7d", + null, + Period.days(7), + Granularities.HOUR + ); + + ReindexingSegmentGranularityRule segGran30d = new ReindexingSegmentGranularityRule( + "seg-gran-30d", + null, + Period.days(30), + Granularities.DAY + ); + + // Data schema rule at P15D (will split the HOUR interval) + ReindexingDataSchemaRule dataSchema15d = new ReindexingDataSchemaRule( + "data-schema-15d", + null, + Period.days(15), + new UserCompactionTaskDimensionsConfig(null), + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + Granularities.MINUTE, + true, + null + ); + + // Deletion rules at different periods + ReindexingDeletionRule deletion10d = new ReindexingDeletionRule( + "deletion-10d", + null, + Period.days(10), + new EqualityFilter("country", ColumnType.STRING, "US", null), + null + ); + + ReindexingDeletionRule deletion20d = new ReindexingDeletionRule( + "deletion-20d", + null, + Period.days(20), + new EqualityFilter("device", ColumnType.STRING, "mobile", null), + null + ); + + // Tuning and IO rules + ReindexingTuningConfigRule tuning7d = new ReindexingTuningConfigRule( + "tuning-7d", + null, + Period.days(7), + new UserCompactionTaskQueryTuningConfig( + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null + ) + ); + + ReindexingIOConfigRule io7d = new ReindexingIOConfigRule( + "io-7d", + null, + Period.days(7), + new UserCompactionTaskIOConfig(true) + ); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of(segGran7d, segGran30d)) + .dataSchemaRules(List.of(dataSchema15d)) + .deletionRules(List.of(deletion10d, deletion20d)) + .tuningConfigRules(List.of(tuning7d)) + .ioConfigRules(List.of(io7d)) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); + + // Verify basic timeline properties + Assert.assertEquals("testDS", timeline.getDataSource()); + Assert.assertEquals(referenceTime, timeline.getReferenceTime()); + Assert.assertNull(timeline.getValidationError()); + Assert.assertNull(timeline.getSkipOffset()); + + // Verify we have multiple intervals (splitting should occur) + Assert.assertTrue("Expected at least 2 intervals", timeline.getIntervals().size() >= 2); + + // Verify each interval has correct structure + for (ReindexingTimelineView.IntervalConfig intervalConfig : timeline.getIntervals()) { + Assert.assertNotNull(intervalConfig.getInterval()); + Assert.assertTrue("Rule count should be > 0", intervalConfig.getRuleCount() > 0); + Assert.assertNotNull(intervalConfig.getConfig()); + Assert.assertNotNull(intervalConfig.getAppliedRules()); + Assert.assertEquals( + "Applied rules size should match rule count", + intervalConfig.getRuleCount(), + intervalConfig.getAppliedRules().size() + ); + + // Verify config has expected components + DataSourceCompactionConfig config = intervalConfig.getConfig(); + Assert.assertNotNull("Should have granularity spec", config.getGranularitySpec()); + Assert.assertNotNull("Should have segment granularity", config.getGranularitySpec().getSegmentGranularity()); + + // Verify appliedRules contain expected rule types + boolean hasTuningRule = intervalConfig.getAppliedRules().stream() + .anyMatch(r -> r instanceof ReindexingTuningConfigRule); + boolean hasIORule = intervalConfig.getAppliedRules().stream() + .anyMatch(r -> r instanceof ReindexingIOConfigRule); + boolean hasDataSchemaRule = intervalConfig.getAppliedRules().stream() + .anyMatch(r -> r instanceof ReindexingDataSchemaRule); + boolean hasDeletionRule = intervalConfig.getAppliedRules().stream() + .anyMatch(r -> r instanceof ReindexingDeletionRule); + boolean hasSegmentGranRule = intervalConfig.getAppliedRules().stream() + .anyMatch(r -> r instanceof ReindexingSegmentGranularityRule); + + // Most recent intervals should have more rules applied + if (intervalConfig.getInterval().getEnd().isAfter(referenceTime.minusDays(10))) { + Assert.assertTrue("Recent intervals should have tuning rules", hasTuningRule); + Assert.assertTrue("Recent intervals should have IO rules", hasIORule); + } + } + } + + /** + * Test that skipOffsetFromNow correctly clamps intervals and populates skipOffset.applied + */ + @Test + public void test_getReindexingTimelineView_skipOffsetFromNow_clampsIntervals() + { + DateTime referenceTime = DateTimes.of("2025-01-29T00:00:00Z"); + Period skipOffset = Period.days(10); + + // Create rules where the most recent rule has a period SMALLER than the skip offset + // This ensures the interval would extend beyond the effectiveEndTime and get clamped + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("seg-3d", null, Period.days(3), Granularities.HOUR), + new ReindexingSegmentGranularityRule("seg-30d", null, Period.days(30), Granularities.DAY) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + skipOffset, // skipOffsetFromNow + Granularities.DAY + ); + + ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); + + // Verify skipOffset is applied + Assert.assertNotNull("Skip offset should be present", timeline.getSkipOffset()); + Assert.assertNotNull("Skip offset should be applied", timeline.getSkipOffset().getApplied()); + Assert.assertNull("Skip offset notApplied should be null", timeline.getSkipOffset().getNotApplied()); + + ReindexingTimelineView.AppliedSkipOffset applied = timeline.getSkipOffset().getApplied(); + Assert.assertEquals("skipOffsetFromNow", applied.getType()); + Assert.assertEquals(skipOffset, applied.getPeriod()); + + DateTime expectedEffectiveEndTime = referenceTime.minus(skipOffset); + Assert.assertEquals(expectedEffectiveEndTime, applied.getEffectiveEndTime()); + + // Verify all intervals are clamped to effectiveEndTime + for (ReindexingTimelineView.IntervalConfig intervalConfig : timeline.getIntervals()) { + Assert.assertTrue( + "Interval end should not exceed effective end time: " + intervalConfig.getInterval(), + !intervalConfig.getInterval().getEnd().isAfter(expectedEffectiveEndTime) + ); + } + + // Verify most recent interval ends exactly at effectiveEndTime (it gets clamped) + if (!timeline.getIntervals().isEmpty()) { + ReindexingTimelineView.IntervalConfig mostRecentInterval = + timeline.getIntervals().get(timeline.getIntervals().size() - 1); + + // The 3-day rule would normally create an interval ending at (referenceTime - 3 days), + // but since that's after effectiveEndTime, it gets clamped to effectiveEndTime + Assert.assertEquals( + "Most recent interval should be clamped to effective end time", + expectedEffectiveEndTime, + mostRecentInterval.getInterval().getEnd() + ); + } + } + + /** + * Test validation error when granularity timeline is invalid + */ + @Test + public void test_getReindexingTimelineView_validationError_invalidGranularityTimeline() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + // Create rules that violate the granularity constraint: + // Older data (P90D) has DAY granularity, newer data (P30D) has HOUR granularity + // This means as we move from past to present, granularity gets finer (valid) + // But then if we add MONTH for recent data, it becomes coarser (invalid) + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) + )) + .dataSchemaRules(List.of( + // This will trigger prepending an interval with default granularity (MONTH) + // which is coarser than HOUR, causing validation failure + new ReindexingDataSchemaRule( + "metrics-7d", + null, + Period.days(7), + null, + new AggregatorFactory[]{new CountAggregatorFactory("count")}, + null, + null, + null + ) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.MONTH // This is coarser than HOUR, will cause validation error + ); + + ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); + + // Verify validation error is present + Assert.assertNotNull("Validation error should be present", timeline.getValidationError()); + Assert.assertEquals( + "INVALID_GRANULARITY_TIMELINE", + timeline.getValidationError().getErrorType() + ); + Assert.assertNotNull(timeline.getValidationError().getMessage()); + Assert.assertTrue( + timeline.getValidationError().getMessage().contains("Invalid segment granularity timeline") + ); + + // Verify structured error information is populated + Assert.assertNotNull(timeline.getValidationError().getOlderInterval()); + Assert.assertNotNull(timeline.getValidationError().getOlderGranularity()); + Assert.assertNotNull(timeline.getValidationError().getNewerInterval()); + Assert.assertNotNull(timeline.getValidationError().getNewerGranularity()); + + // Verify intervals list is empty when validation fails + Assert.assertTrue("Intervals should be empty on validation error", timeline.getIntervals().isEmpty()); + } + + /** + * Test graceful handling when rule provider is not ready + */ + @Test + public void test_getReindexingTimelineView_ruleProviderNotReady() + { + DateTime referenceTime = DateTimes.of("2025-01-29T00:00:00Z"); + + // Create a mock provider that is not ready + ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.expect(mockProvider.isReady()).andReturn(false).anyTimes(); + EasyMock.expect(mockProvider.getType()).andReturn("mock").anyTimes(); + EasyMock.replay(mockProvider); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + mockProvider, + null, + null, + null, + null, + Granularities.DAY + ); + + ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); + + // Verify timeline is returned with empty intervals + Assert.assertNotNull(timeline); + Assert.assertEquals("testDS", timeline.getDataSource()); + Assert.assertEquals(referenceTime, timeline.getReferenceTime()); + Assert.assertTrue("Intervals should be empty", timeline.getIntervals().isEmpty()); + Assert.assertNull("No validation error", timeline.getValidationError()); + Assert.assertNull("No skip offset", timeline.getSkipOffset()); + } + private DruidInputSource createMockSource() { final Interval[] capturedInterval = new Interval[1]; diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index 4f38dfb13004..928c62a6084b 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -509,40 +509,14 @@ function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { } function getRuleTypeName(rule: any): string { - // Infer rule type from its properties since there's no explicit type field - - // DeletionRule has a required 'deleteWhere' field - if (rule.deleteWhere) { - return 'Deletion Rules'; - } - - // DataSchemaRule has these fields - if (rule.dimensionsSpec || rule.metricsSpec || rule.projections || rule.queryGranularity !== undefined) { - return 'Data Schema Rules'; - } - - // SegmentGranularityRule has segmentGranularity - if (rule.segmentGranularity) { - return 'Segment Granularity Rules'; - } - - // TuningConfigRule has tuningConfig - if (rule.tuningConfig) { - return 'Tuning Config Rules'; - } - - // IOConfigRule has ioConfig - if (rule.ioConfig) { - return 'IO Config Rules'; - } - - // Fallback: use the id to infer type if it contains type info - const id = rule.id || ''; - if (id.toLowerCase().includes('delete')) return 'Deletion Rules'; - if (id.toLowerCase().includes('dataschema')) return 'Data Schema Rules'; - if (id.toLowerCase().includes('granularity')) return 'Segment Granularity Rules'; - if (id.toLowerCase().includes('tuning')) return 'Tuning Config Rules'; - if (id.toLowerCase().includes('io')) return 'IO Config Rules'; + // Use the explicit type field provided by Jackson serialization + const typeMap: Record = { + 'deletion': 'Deletion Rules', + 'dataSchema': 'Data Schema Rules', + 'segmentGranularity': 'Segment Granularity Rules', + 'tuningConfig': 'Tuning Config Rules', + 'ioConfig': 'IO Config Rules', + }; - return 'Other Rules'; + return typeMap[rule.type] || 'Other Rules'; } From 091a0d3dd1234e9b2bf0d43f21328101f38dd880 Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 15 Feb 2026 18:04:20 -0600 Subject: [PATCH 56/90] bug fixes for timeline --- .../compact/CascadingReindexingTemplate.java | 182 +++++++--------- .../CascadingReindexingTemplateTest.java | 196 ++++++++---------- .../compaction/IntervalGranularityInfo.java | 63 ++++++ .../compaction/ReindexingConfigBuilder.java | 57 +++-- .../ReindexingConfigBuilderTest.java | 33 ++- .../reindexing-timeline.tsx | 40 ++-- 6 files changed, 328 insertions(+), 243 deletions(-) create mode 100644 server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 63c265f94fa8..3e7c1fdfd325 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -36,6 +36,7 @@ import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.compaction.CompactionCandidate; import org.apache.druid.server.compaction.CompactionStatus; +import org.apache.druid.server.compaction.IntervalGranularityInfo; import org.apache.druid.server.compaction.ReindexingConfigBuilder; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; @@ -55,6 +56,7 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -291,7 +293,7 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); } - List searchIntervals; + List searchIntervals; try { searchIntervals = generateAlignedSearchIntervals(referenceTime); } @@ -351,30 +353,35 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) // Build configs for each interval List intervalConfigs = new ArrayList<>(); - for (Interval searchInterval : searchIntervals) { - // Clamp interval to effective end time - Interval clampedInterval = searchInterval; + for (IntervalGranularityInfo intervalInfo : searchIntervals) { + Interval searchInterval = intervalInfo.getInterval(); + + // Check if interval extends past skip offset if (searchInterval.getEnd().isAfter(effectiveEndTime)) { - if (searchInterval.getStart().isBefore(effectiveEndTime)) { - clampedInterval = new Interval(searchInterval.getStart(), effectiveEndTime); - } else { - // Entire interval is beyond skip offset, skip it - continue; - } + // Include in timeline but mark as skipped (no rules applied) + intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( + searchInterval, + 0, // ruleCount = 0 indicates no rules applied (skipped due to skip offset) + null, // no config + Collections.emptyList() // no applied rules + )); + continue; } + // Process intervals within skip offset normally InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( ruleProvider, defaultSegmentGranularity, - clampedInterval, - referenceTime + searchInterval, + referenceTime, + searchIntervals // Pass synthetic timeline ); ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builder); if (buildResult.getRuleCount() > 0) { intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( - clampedInterval, + searchInterval, buildResult.getRuleCount(), builder.build(), buildResult.getAppliedRules() @@ -411,7 +418,7 @@ public List createCompactionJobs( return Collections.emptyList(); } - List searchIntervals = generateAlignedSearchIntervals(currentTime); + List searchIntervals = generateAlignedSearchIntervals(currentTime); if (searchIntervals.isEmpty()) { LOG.warn("No search intervals generated for dataSource[%s], no reindexing jobs will be created", dataSource); return Collections.emptyList(); @@ -427,7 +434,8 @@ public List createCompactionJobs( return Collections.emptyList(); } - for (Interval reindexingInterval : searchIntervals) { + for (IntervalGranularityInfo intervalInfo : searchIntervals) { + Interval reindexingInterval = intervalInfo.getInterval(); if (!reindexingInterval.overlaps(adjustedTimelineInterval)) { // No underlying data exists to reindex for this interval @@ -435,9 +443,13 @@ public List createCompactionJobs( continue; } - reindexingInterval = clampIntervalToBounds(reindexingInterval, adjustedTimelineInterval); - if (reindexingInterval == null) { - LOG.warn("Clamped reindexing interval is invalid or empty after applying bounds, meaning no search interval exists. Skipping."); + // Skip intervals that extend past the skip offset boundary (not just data boundary) + // This preserves granularity alignment and ensures intervals exist in synthetic timeline + // Only apply this when a skip offset is actually configured + if ((skipOffsetFromNow != null || skipOffsetFromLatest != null) && + reindexingInterval.getEnd().isAfter(adjustedTimelineInterval.getEnd())) { + LOG.debug("Search interval[%s] extends past skip offset boundary[%s], skipping to preserve alignment", + reindexingInterval, adjustedTimelineInterval.getEnd()); continue; } @@ -447,7 +459,8 @@ public List createCompactionJobs( ruleProvider, defaultSegmentGranularity, reindexingInterval, - currentTime + currentTime, + searchIntervals // Pass synthetic timeline ); int ruleCount = configBuilder.applyTo(builder); @@ -476,46 +489,6 @@ protected CompactionJobTemplate createJobTemplateForInterval( return new CompactionConfigBasedJobTemplate(config, createCascadingFinalizer()); } - /** - * Clamps an interval to fit within the specified bounds by adjusting start and/or end times. - * If the interval extends beyond the bounds, it is trimmed to fit. Returns null if the - * resulting interval would be invalid (end before start). - * - * @param interval the interval to clamp - * @param bounds the bounds to clamp to - * @return the clamped interval, or null if the result would be invalid or empty - */ - @Nullable - private Interval clampIntervalToBounds(Interval interval, Interval bounds) - { - DateTime start = interval.getStart(); - DateTime end = interval.getEnd(); - - if (start.isBefore(bounds.getStart())) { - LOG.debug( - "Adjusting start of search interval[%s] to match bounds start[%s]", - interval, - bounds.getStart() - ); - start = bounds.getStart(); - } - - if (end.isAfter(bounds.getEnd())) { - LOG.debug( - "Adjusting end of search interval[%s] to match bounds end[%s]", - interval, - bounds.getEnd() - ); - end = bounds.getEnd(); - } - - if (end.isBefore(start) || end.isEqual(start)) { - return null; - } - - return new Interval(start, end); - } - /** * Applies the configured skip offset to an interval by adjusting its end time. Uses either * skipOffsetFromNow (relative to reference time) or skipOffsetFromLatest (relative to interval end). @@ -573,30 +546,30 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() * * * @param referenceTime the reference time for calculating period thresholds - * @return list of split and aligned intervals, ordered from oldest to newest + * @return list of split and aligned intervals with their granularities and source rules, ordered from oldest to newest * @throws IAE if no segment granularity rules are found */ - List generateAlignedSearchIntervals(DateTime referenceTime) + List generateAlignedSearchIntervals(DateTime referenceTime) { - List baseTimeline = generateBaseSegmentGranularityAlignedTimeline(referenceTime); + List baseTimeline = generateBaseSegmentGranularityAlignedTimeline(referenceTime); List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); - List finalIntervals = new ArrayList<>(); + List finalIntervals = new ArrayList<>(); - for (IntervalWithGranularity baseInterval : baseTimeline) { + for (IntervalGranularityInfo baseInterval : baseTimeline) { List splitPoints = new ArrayList<>(); for (DateTime threshold : nonSegmentGranThresholds) { - if (threshold.isAfter(baseInterval.interval.getStart()) && - threshold.isBefore(baseInterval.interval.getEnd())) { + if (threshold.isAfter(baseInterval.getInterval().getStart()) && + threshold.isBefore(baseInterval.getInterval().getEnd())) { // Align threshold to this interval's segment granularity - DateTime alignedThreshold = baseInterval.granularity.bucketStart(threshold); + DateTime alignedThreshold = baseInterval.getGranularity().bucketStart(threshold); // Only add if it's not at the boundaries (would create zero-length interval) - if (alignedThreshold.isAfter(baseInterval.interval.getStart()) && - alignedThreshold.isBefore(baseInterval.interval.getEnd())) { + if (alignedThreshold.isAfter(baseInterval.getInterval().getStart()) && + alignedThreshold.isBefore(baseInterval.getInterval().getEnd())) { splitPoints.add(alignedThreshold); } } @@ -607,18 +580,26 @@ List generateAlignedSearchIntervals(DateTime referenceTime) .sorted() .collect(Collectors.toList()); - // Split this base interval at the split points + // Split this base interval at the split points, preserving granularity and source rule if (splitPoints.isEmpty()) { - LOG.debug("No splits for interval [%s]", baseInterval.interval); - finalIntervals.add(baseInterval.interval); + LOG.debug("No splits for interval [%s]", baseInterval.getInterval()); + finalIntervals.add(baseInterval); } else { - LOG.debug("Splitting interval [%s] at [%d] points", baseInterval.interval, splitPoints.size()); - DateTime start = baseInterval.interval.getStart(); + LOG.debug("Splitting interval [%s] at [%d] points", baseInterval.getInterval(), splitPoints.size()); + DateTime start = baseInterval.getInterval().getStart(); for (DateTime splitPoint : splitPoints) { - finalIntervals.add(new Interval(start, splitPoint)); + finalIntervals.add(new IntervalGranularityInfo( + new Interval(start, splitPoint), + baseInterval.getGranularity(), + baseInterval.getSourceRule() // Preserve source rule from base interval + )); start = splitPoint; } - finalIntervals.add(new Interval(start, baseInterval.interval.getEnd())); + finalIntervals.add(new IntervalGranularityInfo( + new Interval(start, baseInterval.getInterval().getEnd()), + baseInterval.getGranularity(), + baseInterval.getSourceRule() // Preserve source rule from base interval + )); } } @@ -650,7 +631,7 @@ List generateAlignedSearchIntervals(DateTime referenceTime) * * */ - private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) + private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) { List segmentGranRules = ruleProvider.getSegmentGranularityRules(); List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); @@ -677,9 +658,10 @@ private List generateBaseSegmentGranularityAlignedTimel alignedEnd ); - return Collections.singletonList(new IntervalWithGranularity( + return Collections.singletonList(new IntervalGranularityInfo( new Interval(DateTimes.MIN, alignedEnd), - defaultSegmentGranularity + defaultSegmentGranularity, + null // No source rule when using default granularity )); } @@ -692,7 +674,7 @@ private List generateBaseSegmentGranularityAlignedTimel .collect(Collectors.toList()); // Build base timeline with granularities tracked - List baseTimeline = new ArrayList<>(); + List baseTimeline = new ArrayList<>(); DateTime previousAlignedEnd = null; for (ReindexingSegmentGranularityRule rule : sortedRules) { @@ -709,9 +691,10 @@ private List generateBaseSegmentGranularityAlignedTimel rule.getSegmentGranularity() ); - baseTimeline.add(new IntervalWithGranularity( + baseTimeline.add(new IntervalGranularityInfo( new Interval(alignedStart, alignedEnd), - rule.getSegmentGranularity() + rule.getSegmentGranularity(), + rule // Track the source rule )); previousAlignedEnd = alignedEnd; @@ -721,7 +704,7 @@ private List generateBaseSegmentGranularityAlignedTimel // than the most recent segment gran rule if (!nonSegmentGranThresholds.isEmpty()) { DateTime mostRecentNonSegmentGranThreshold = Collections.max(nonSegmentGranThresholds); - DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).interval.getEnd(); + DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).getInterval().getEnd(); if (mostRecentNonSegmentGranThreshold.isAfter(mostRecentSegmentGranEnd)) { DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); @@ -745,9 +728,10 @@ private List generateBaseSegmentGranularityAlignedTimel alignedEnd ); - baseTimeline.add(new IntervalWithGranularity( + baseTimeline.add(new IntervalGranularityInfo( new Interval(mostRecentSegmentGranEnd, alignedEnd), - defaultSegmentGranularity + defaultSegmentGranularity, + null // No source rule when using default granularity )); } } @@ -770,18 +754,18 @@ private List generateBaseSegmentGranularityAlignedTimel * @param timeline the completed base timeline with granularity information * @throws IAE if granularity becomes coarser as we move toward present */ - private void validateSegmentGranularityTimeline(List timeline) + private void validateSegmentGranularityTimeline(List timeline) { if (timeline.size() <= 1) { return; // Nothing to validate } for (int i = 1; i < timeline.size(); i++) { - IntervalWithGranularity olderInterval = timeline.get(i - 1); - IntervalWithGranularity newerInterval = timeline.get(i); + IntervalGranularityInfo olderInterval = timeline.get(i - 1); + IntervalGranularityInfo newerInterval = timeline.get(i); - Granularity olderGran = olderInterval.granularity; - Granularity newerGran = newerInterval.granularity; + Granularity olderGran = olderInterval.getGranularity(); + Granularity newerGran = newerInterval.getGranularity(); // As we move from past (older intervals) to present (newer intervals), // granularity should stay the same or get finer. @@ -790,9 +774,9 @@ private void validateSegmentGranularityTimeline(List ti if (olderGran.isFinerThan(newerGran)) { throw new GranularityTimelineValidationException( dataSource, - olderInterval.interval, + olderInterval.getInterval(), olderGran, - newerInterval.interval, + newerInterval.getInterval(), newerGran ); } @@ -835,20 +819,6 @@ public CompactionState toCompactionState() throw new UnsupportedOperationException("CascadingReindexingTemplate cannot be transformed to a CompactionState object"); } - /** - * Helper class to track an interval with its associated segment granularity. - */ - private static class IntervalWithGranularity - { - final Interval interval; - final Granularity granularity; - - IntervalWithGranularity(Interval interval, Granularity granularity) - { - this.interval = interval; - this.granularity = granularity; - } - } // Legacy fields from DataSourceCompactionConfig that are not used by this template diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index bbed0ce3d2e2..3a6abaea0c08 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -33,6 +33,7 @@ import org.apache.druid.query.filter.EqualityFilter; import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.IntervalGranularityInfo; import org.apache.druid.server.compaction.ReindexingDataSchemaRule; import org.apache.druid.server.compaction.ReindexingDeletionRule; import org.apache.druid.server.compaction.ReindexingIOConfigRule; @@ -222,10 +223,10 @@ public void test_createCompactionJobs_simple() Assert.assertEquals(2, processedIntervals.size()); // Intervals are now in chronological order (oldest first) - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(10), processedIntervals.get(1).getEnd()); + Assert.assertEquals(referenceTime.minusDays(7), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -252,7 +253,7 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_skipAllOfTime() } @Test - public void test_createCompactionJobs_withSkipOffsetFromLatest_trimsIntervalEnd() + public void test_createCompactionJobs_withSkipOffsetFromLatest_skipsIntervalsExtendingPastOffset() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); @@ -267,12 +268,9 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_trimsIntervalEnd( template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(2, processedIntervals.size()); - // Intervals are now in chronological order (oldest first) - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(1, processedIntervals.size()); + Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(15), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -287,15 +285,15 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_eliminatesInterva DruidInputSource mockSource = createMockSource(); TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( - "testDS", null, null, mockProvider, null, null, Period.days(30), null + "testDS", null, null, mockProvider, null, null, Period.days(15), null ); template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(40), processedIntervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -322,7 +320,7 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_skipAllOfTime() } @Test - public void test_createCompactionJobs_withSkipOffsetFromNow_trimsIntervalEnd() + public void test_createCompactionJobs_withSkipOffsetFromNow_skipsIntervalsExtendingPastOffset() { DateTime referenceTime = DateTimes.of("2024-01-15T00:00:00Z"); SegmentTimeline timeline = createTestTimeline(referenceTime.minusDays(90), referenceTime.minusDays(10)); @@ -337,12 +335,9 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_trimsIntervalEnd() template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(2, processedIntervals.size()); - // Intervals are now in chronological order (oldest first) - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); + Assert.assertEquals(1, processedIntervals.size()); + Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(20), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -357,15 +352,15 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() DruidInputSource mockSource = createMockSource(); TestCascadingReindexingTemplate template = new TestCascadingReindexingTemplate( - "testDS", null, null, mockProvider, null, null, null, Period.days(40) + "testDS", null, null, mockProvider, null, null, null, Period.days(20) ); template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(referenceTime.minusDays(90), processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(40), processedIntervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -427,18 +422,18 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(3, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(2).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(2).getInterval().getEnd()); } /** @@ -509,30 +504,30 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(7, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(2).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(2).getEnd()); + Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(2).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(2).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(3).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(3).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(3).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(3).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(4).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(4).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(5).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(5).getEnd()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(5).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(5).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(6).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getEnd()); + Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(6).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getInterval().getEnd()); } /** @@ -592,18 +587,18 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(3, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(2).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-21T00:00:00Z"), intervals.get(2).getEnd()); + Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(2).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-21T00:00:00Z"), intervals.get(2).getInterval().getEnd()); } /** @@ -675,24 +670,24 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen Granularities.HOUR ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(5, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(2).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(2).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(3).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(3).getEnd()); + Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(3).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(3).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(4).getEnd()); + Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(4).getInterval().getEnd()); } /** @@ -767,27 +762,27 @@ public void test_generateAlignedSearchIntervals() Granularities.HOUR ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(6, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(2).getStart()); - Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(2).getEnd()); + Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(2).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(2).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(3).getStart()); - Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(3).getEnd()); + Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(3).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(3).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(4).getStart()); - Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00Z"), intervals.get(4).getEnd()); + Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(4).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00Z"), intervals.get(4).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00"), intervals.get(5).getStart()); - Assert.assertEquals(DateTimes.of("2024-02-03T22:00:00"), intervals.get(5).getEnd()); + Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00"), intervals.get(5).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-02-03T22:00:00"), intervals.get(5).getInterval().getEnd()); } /** @@ -888,12 +883,12 @@ public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundar Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(1, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2025-01-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2025-01-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); } /** @@ -951,12 +946,12 @@ public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTim Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(1, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-31T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-31T00:00:00Z"), intervals.get(0).getInterval().getEnd()); } /** @@ -1018,15 +1013,15 @@ public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(2, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(1).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getEnd()); + Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(1).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getInterval().getEnd()); } /** @@ -1076,12 +1071,12 @@ public void test_generateAlignedSearchIntervals_singleRuleOnly() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List intervals = template.generateAlignedSearchIntervals(referenceTime); Assert.assertEquals(1, intervals.size()); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getStart()); - Assert.assertEquals(DateTimes.of("2024-12-01T00:00:00Z"), intervals.get(0).getEnd()); + Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); + Assert.assertEquals(DateTimes.of("2024-12-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); } private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate @@ -1176,6 +1171,7 @@ private ReindexingRuleProvider createMockProvider(List periods) EasyMock.expect(mockProvider.getSegmentGranularityRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(segmentGranularityRules.get(0)).anyTimes(); EasyMock.expect(mockProvider.getIOConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getTuningConfigRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); + EasyMock.expect(mockProvider.getDataSchemaRule(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(null).anyTimes(); EasyMock.expect(mockProvider.getDeletionRules(EasyMock.anyObject(), EasyMock.anyObject())).andReturn(Collections.emptyList()).anyTimes(); EasyMock.replay(mockProvider); return mockProvider; @@ -1455,10 +1451,10 @@ public void test_getReindexingTimelineView_comprehensive() } /** - * Test that skipOffsetFromNow correctly clamps intervals and populates skipOffset.applied + * Test that skipOffsetFromNow correctly skips intervals and populates skipOffset.applied */ @Test - public void test_getReindexingTimelineView_skipOffsetFromNow_clampsIntervals() + public void test_getReindexingTimelineView_skipOffsetFromNow_skipsProperIntervals() { DateTime referenceTime = DateTimes.of("2025-01-29T00:00:00Z"); Period skipOffset = Period.days(10); @@ -1498,26 +1494,10 @@ public void test_getReindexingTimelineView_skipOffsetFromNow_clampsIntervals() DateTime expectedEffectiveEndTime = referenceTime.minus(skipOffset); Assert.assertEquals(expectedEffectiveEndTime, applied.getEffectiveEndTime()); - // Verify all intervals are clamped to effectiveEndTime for (ReindexingTimelineView.IntervalConfig intervalConfig : timeline.getIntervals()) { - Assert.assertTrue( - "Interval end should not exceed effective end time: " + intervalConfig.getInterval(), - !intervalConfig.getInterval().getEnd().isAfter(expectedEffectiveEndTime) - ); - } - - // Verify most recent interval ends exactly at effectiveEndTime (it gets clamped) - if (!timeline.getIntervals().isEmpty()) { - ReindexingTimelineView.IntervalConfig mostRecentInterval = - timeline.getIntervals().get(timeline.getIntervals().size() - 1); - - // The 3-day rule would normally create an interval ending at (referenceTime - 3 days), - // but since that's after effectiveEndTime, it gets clamped to effectiveEndTime - Assert.assertEquals( - "Most recent interval should be clamped to effective end time", - expectedEffectiveEndTime, - mostRecentInterval.getInterval().getEnd() - ); + if (intervalConfig.getInterval().getEnd().isAfter(expectedEffectiveEndTime)) { + Assert.assertEquals(0, intervalConfig.getRuleCount()); + } } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java b/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java new file mode 100644 index 000000000000..2f9df47ce77c --- /dev/null +++ b/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java @@ -0,0 +1,63 @@ +/* + * 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.compaction; + +import org.apache.druid.java.util.common.granularity.Granularity; +import org.joda.time.Interval; + +import javax.annotation.Nullable; + +/** + * Associates a time interval with its segment granularity and optional source rule. + * Used to pass synthetic timeline information from timeline generation to config building. + */ +public class IntervalGranularityInfo +{ + private final Interval interval; + private final Granularity granularity; + private final ReindexingSegmentGranularityRule sourceRule; + + public IntervalGranularityInfo( + Interval interval, + Granularity granularity, + @Nullable ReindexingSegmentGranularityRule sourceRule + ) + { + this.interval = interval; + this.granularity = granularity; + this.sourceRule = sourceRule; + } + + public Interval getInterval() + { + return interval; + } + + public Granularity getGranularity() + { + return granularity; + } + + @Nullable + public ReindexingSegmentGranularityRule getSourceRule() + { + return sourceRule; + } +} diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index 895e20e9b260..d9e72f1ff1be 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -19,6 +19,7 @@ package org.apache.druid.server.compaction; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; @@ -50,6 +51,7 @@ public class ReindexingConfigBuilder private final Granularity defaultGranularity; private final Interval interval; private final DateTime referenceTime; + private final List syntheticTimeline; /** * Result of applying reindexing rules to a config builder. @@ -85,14 +87,17 @@ public List getAppliedRules() public ReindexingConfigBuilder( ReindexingRuleProvider provider, - Granularity defaultSegmentGranularity, Interval interval, - DateTime referenceTime + Granularity defaultSegmentGranularity, + Interval interval, + DateTime referenceTime, + @Nullable List syntheticTimeline ) { this.provider = provider; this.defaultGranularity = defaultSegmentGranularity; this.interval = interval; this.referenceTime = referenceTime; + this.syntheticTimeline = syntheticTimeline; } /** @@ -149,19 +154,47 @@ public BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Bui } // Apply segment granularity rule - ReindexingSegmentGranularityRule segmentGranularityRule = provider.getSegmentGranularityRule(interval, referenceTime); - if (segmentGranularityRule == null) { - if (count > 0) { - // Insert a default segment granularity to the config only if other rules exist for the interval. - builder.withSegmentGranularity(defaultGranularity); - } - } else { - builder.withSegmentGranularity(segmentGranularityRule.getSegmentGranularity()); - appliedRules.add(segmentGranularityRule); + // Use granularity from synthetic timeline + IntervalGranularityInfo granularityInfo = findMatchingInterval(interval); + if (granularityInfo == null) { + throw DruidException.defensive( + "No matching interval found in synthetic timeline for interval[%s]. This should never happen.", + interval + ); + } + + builder.withSegmentGranularity(granularityInfo.getGranularity()); + if (granularityInfo.getSourceRule() != null) { + // Only count and track the rule if it came from an actual rule (not default) + appliedRules.add(granularityInfo.getSourceRule()); count++; } - return new BuildResult(count, appliedRules); + if (count == 0) { + return new BuildResult(0, List.of()); + } else { + return new BuildResult(count, appliedRules); + } + } + + /** + * Finds the matching interval granularity info from the synthetic timeline. + * Returns null if no synthetic timeline was provided or no match is found. + */ + @Nullable + private IntervalGranularityInfo findMatchingInterval(Interval interval) + { + if (syntheticTimeline == null) { + return null; + } + + for (IntervalGranularityInfo candidate : syntheticTimeline) { + if (candidate.getInterval().equals(interval)) { + return candidate; + } + } + + return null; } private void applyDataSchemaRule( diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 2aaa2c1be45d..544d76febd09 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -68,11 +68,17 @@ public void test_applyTo_handlesSynteticSegmentGranularityInsertion() InlineSchemaDataSourceCompactionConfig.builder() .forDataSource("test_datasource"); + // Create synthetic timeline with default granularity (no source rule since it's default) + ImmutableList syntheticTimeline = ImmutableList.of( + new IntervalGranularityInfo(TEST_INTERVAL, Granularities.DAY, null) + ); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, Granularities.DAY, TEST_INTERVAL, - REFERENCE_TIME + REFERENCE_TIME, + syntheticTimeline ); int count = configBuilder.applyTo(builder); @@ -116,11 +122,25 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount InlineSchemaDataSourceCompactionConfig.builder() .forDataSource("test_datasource"); + // Create the segment granularity rule used in the provider + ReindexingSegmentGranularityRule segmentGranularityRule = new ReindexingSegmentGranularityRule( + "gran-30d", + null, + Period.days(30), + Granularities.DAY + ); + + // Create synthetic timeline with granularity from the rule + ImmutableList syntheticTimeline = ImmutableList.of( + new IntervalGranularityInfo(TEST_INTERVAL, Granularities.DAY, segmentGranularityRule) + ); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, Granularities.DAY, TEST_INTERVAL, - REFERENCE_TIME + REFERENCE_TIME, + syntheticTimeline ); int count = configBuilder.applyTo(builder); @@ -193,11 +213,17 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() InlineSchemaDataSourceCompactionConfig.builder() .forDataSource("test_datasource"); + // Create synthetic timeline with default granularity (no source rule) + ImmutableList syntheticTimeline = ImmutableList.of( + new IntervalGranularityInfo(TEST_INTERVAL, Granularities.DAY, null) + ); + ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, Granularities.DAY, TEST_INTERVAL, - REFERENCE_TIME + REFERENCE_TIME, + syntheticTimeline ); int count = configBuilder.applyTo(builder); @@ -206,7 +232,6 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() InlineSchemaDataSourceCompactionConfig config = builder.build(); - Assert.assertNull(config.getGranularitySpec()); Assert.assertNull(config.getTuningConfig()); Assert.assertNull(config.getMetricsSpec()); Assert.assertNull(config.getDimensionsSpec()); diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index 928c62a6084b..fe9529bec48d 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -197,9 +197,19 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( )} {skipOffset.notApplied && ( - - {skipOffset.notApplied.type} not applied: {skipOffset.notApplied.reason} - + + + {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in this preview + + )}
    )} @@ -210,24 +220,28 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( {intervals.map((interval, idx) => { const [start, end] = interval.interval.split('/'); const isSelected = selectedIntervalIndex === idx; + const isSkipped = interval.ruleCount === 0; return (
    setSelectedIntervalIndex(idx)} - title={`${interval.interval}\n${interval.ruleCount} rule(s) applied`} + onClick={() => !isSkipped && setSelectedIntervalIndex(idx)} + title={isSkipped + ? `${interval.interval}\nSkipped (beyond skip offset)` + : `${interval.interval}\n${interval.ruleCount} rule(s) applied`} >
    {formatDateShort(start)}
    {formatDateShort(end)}
    - - {interval.ruleCount} rule{interval.ruleCount !== 1 ? 's' : ''} + + {isSkipped ? 'skipped' : `${interval.ruleCount} rule${interval.ruleCount !== 1 ? 's' : ''}`}
    @@ -240,7 +254,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline(
    - {selectedInterval && ( + {selectedInterval && selectedInterval.ruleCount > 0 && ( setSelectedIntervalIndex(undefined)} @@ -258,9 +272,9 @@ function formatDateShort(isoDate: string): string { const date = new Date(isoDate); - const month = date.toLocaleString('default', { month: 'short' }); - const day = date.getDate(); - const year = date.getFullYear(); + const month = date.toLocaleString('default', { month: 'short', timeZone: 'UTC' }); + const day = date.getUTCDate(); + const year = date.getUTCFullYear(); return `${month} ${day}, ${year}`; } From 3dd6069d95a45320760fd74b8a3fc0c56afa66b3 Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 15 Feb 2026 18:17:26 -0600 Subject: [PATCH 57/90] Support dynamic discovery of skip intervals when skipOffsetFromLatest is available --- .../compact/CascadingReindexingTemplate.java | 60 +++++++-- .../compaction/ReindexingConfigBuilder.java | 28 +++- .../reindexing-timeline.tsx | 124 ++++++++++++++---- 3 files changed, 173 insertions(+), 39 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 3e7c1fdfd325..fcb7714394ba 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -56,7 +56,6 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Collections; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -274,6 +273,45 @@ private static boolean shouldOptimizeFilterRules( return filter instanceof NotDimFilter; } + /** + * Creates a validation error view for timeline generation failures. + * Logs the exception and returns a timeline view containing the validation error details. + */ + private ReindexingTimelineView createValidationErrorView( + Exception e, + DateTime referenceTime, + String errorType, + @Nullable String olderInterval, + @Nullable String olderGranularity, + @Nullable String newerInterval, + @Nullable String newerGranularity + ) + { + LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); + ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( + errorType, + e.getMessage(), + olderInterval, + olderGranularity, + newerInterval, + newerGranularity + ); + return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); + } + + /** + * Checks if the given interval's end time is after the specified boundary. + * Used to determine if intervals should be skipped based on skip offset configuration. + * + * @param interval the interval to check + * @param boundary the boundary time to compare against + * @return true if the interval ends after the boundary + */ + private static boolean intervalEndsAfter(Interval interval, DateTime boundary) + { + return interval.getEnd().isAfter(boundary); + } + /** * Generates a timeline view showing the search intervals and their associated reindexing * configurations. This is useful for operators to understand how rules are applied across @@ -298,30 +336,26 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) searchIntervals = generateAlignedSearchIntervals(referenceTime); } catch (GranularityTimelineValidationException e) { - // Validation failed - extract structured error details and return with validation error - LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); - ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( + return createValidationErrorView( + e, + referenceTime, "INVALID_GRANULARITY_TIMELINE", - e.getMessage(), e.getOlderInterval().toString(), e.getOlderGranularity().toString(), e.getNewerInterval().toString(), e.getNewerGranularity().toString() ); - return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); } catch (IAE e) { - // Other validation errors (e.g., no rules configured) - LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); - ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( + return createValidationErrorView( + e, + referenceTime, "VALIDATION_ERROR", - e.getMessage(), null, null, null, null ); - return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); } if (searchIntervals.isEmpty()) { @@ -357,7 +391,7 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) Interval searchInterval = intervalInfo.getInterval(); // Check if interval extends past skip offset - if (searchInterval.getEnd().isAfter(effectiveEndTime)) { + if (intervalEndsAfter(searchInterval, effectiveEndTime)) { // Include in timeline but mark as skipped (no rules applied) intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( searchInterval, @@ -447,7 +481,7 @@ public List createCompactionJobs( // This preserves granularity alignment and ensures intervals exist in synthetic timeline // Only apply this when a skip offset is actually configured if ((skipOffsetFromNow != null || skipOffsetFromLatest != null) && - reindexingInterval.getEnd().isAfter(adjustedTimelineInterval.getEnd())) { + intervalEndsAfter(reindexingInterval, adjustedTimelineInterval.getEnd())) { LOG.debug("Search interval[%s] extends past skip offset boundary[%s], skipping to preserve alignment", reindexingInterval, adjustedTimelineInterval.getEnd()); continue; diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index d9e72f1ff1be..7820b7c8897e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -69,7 +69,11 @@ public BuildResult(int ruleCount, List appliedRules) } /** - * @return the number of rules that were applied + * Returns the count of rules that were actually applied to this specific interval. + * This is NOT the total number of rules in the provider, but rather the count + * of rules that matched and were applied during config building. + * + * @return the number of rules that were applied to the builder */ public int getRuleCount() { @@ -222,6 +226,28 @@ private void applyDataSchemaRule( } } + /** + * Applies deletion rules by combining their filters into a single transform filter. + *

    + * Each deletion rule specifies rows that should be deleted. To implement deletion during + * compaction, we need to keep only rows that do NOT match any deletion rule. + *

    + * Filter construction logic: + *

      + *
    • Collect all deletion filters (one per rule)
    • + *
    • OR them together: (filter1 OR filter2 OR ...)
    • + *
    • Wrap in NOT: NOT(filter1 OR filter2 OR ...)
    • + *
    + *

    + * Result: Rows matching ANY deletion rule are filtered out, all other rows are kept. + *

    + * Example: With rules "delete country=US" and "delete device=mobile": + * Final filter: NOT((country=US) OR (device=mobile)) + * This keeps all rows except those where country=US OR device=mobile. + * + * @param builder the config builder to apply the deletion filter to + * @param rules the deletion rules to combine + */ private void applyDeletionRulesList( InlineSchemaDataSourceCompactionConfig.Builder builder, List rules diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index fe9529bec48d..6f4cd2b0ceeb 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -29,6 +29,16 @@ import { Loader } from '../loader/loader'; import './reindexing-timeline.scss'; +const TIMELINE_INTERVAL_COLORS = [ + '#5C7080', // gray + '#738694', // light gray + '#8A9BA8', // lighter gray + '#394B59', // dark gray + '#4A5568', // medium dark gray + '#6B7E91', // medium gray + '#2F343C', // darker gray +]; + interface ReindexingTimelineProps { supervisorId: string; } @@ -75,6 +85,8 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( ) { const { supervisorId } = props; const [selectedIntervalIndex, setSelectedIntervalIndex] = useState(); + const [queriedMaxTime, setQueriedMaxTime] = useState(); + const [queryingMaxTime, setQueryingMaxTime] = useState(false); const [timelineState] = useQueryManager({ query: supervisorId, @@ -108,6 +120,55 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( const { intervals, skipOffset, referenceTime, validationError } = timelineData; + const handleQueryMaxTime = async () => { + setQueryingMaxTime(true); + try { + const query = { + queryType: 'timeBoundary', + dataSource: timelineData.dataSource, + }; + const resp = await Api.instance.post('/druid/v2', query); + const result = resp.data; + if (result && result.length > 0 && result[0].result) { + const maxTime = result[0].result.maxTime; + setQueriedMaxTime(maxTime); + } else { + AppToaster.show({ + message: 'No data found in datasource', + intent: Intent.WARNING, + }); + } + } catch (e) { + AppToaster.show({ + message: `Failed to query max time: ${e.message}`, + intent: Intent.DANGER, + }); + } finally { + setQueryingMaxTime(false); + } + }; + + // Calculate effective end time if we have queried max time and skipOffsetFromLatest + let effectiveEndTime: Date | undefined; + if (queriedMaxTime && skipOffset?.notApplied) { + const maxTime = new Date(queriedMaxTime); + const period = skipOffset.notApplied.period; + effectiveEndTime = new Date(maxTime); + + // Parse ISO 8601 duration format (e.g., "P7D", "P1M", "P1Y", "PT12H", "P1M7D") + const yearMatch = period.match(/(\d+)Y/); + const monthMatch = period.match(/(\d+)M/); + const weekMatch = period.match(/(\d+)W/); + const dayMatch = period.match(/(\d+)D/); + const hourMatch = period.match(/T.*?(\d+)H/); + + if (yearMatch) effectiveEndTime.setFullYear(effectiveEndTime.getFullYear() - parseInt(yearMatch[1], 10)); + if (monthMatch) effectiveEndTime.setMonth(effectiveEndTime.getMonth() - parseInt(monthMatch[1], 10)); + if (weekMatch) effectiveEndTime.setDate(effectiveEndTime.getDate() - parseInt(weekMatch[1], 10) * 7); + if (dayMatch) effectiveEndTime.setDate(effectiveEndTime.getDate() - parseInt(dayMatch[1], 10)); + if (hourMatch) effectiveEndTime.setHours(effectiveEndTime.getHours() - parseInt(hourMatch[1], 10)); + } + // Display validation error if present if (validationError) { return ( @@ -161,16 +222,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( // Generate a color based on interval index using blue/gray scale theme const getIntervalColor = (index: number) => { - const colors = [ - '#5C7080', // gray - '#738694', // light gray - '#8A9BA8', // lighter gray - '#394B59', // dark gray - '#4A5568', // medium dark gray - '#6B7E91', // medium gray - '#2F343C', // darker gray - ]; - return colors[index % colors.length]; + return TIMELINE_INTERVAL_COLORS[index % TIMELINE_INTERVAL_COLORS.length]; }; return ( @@ -196,20 +248,35 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( Skip Offset: {skipOffset.applied.type} ({skipOffset.applied.period}) )} - {skipOffset.notApplied && ( - - - {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in this preview - - + {skipOffset.notApplied && !queriedMaxTime && ( + <> + + + {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in this preview + + +

    )} @@ -220,7 +287,14 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( {intervals.map((interval, idx) => { const [start, end] = interval.interval.split('/'); const isSelected = selectedIntervalIndex === idx; - const isSkipped = interval.ruleCount === 0; + + // Check if interval is skipped due to skip offset (either from API or from queried max time) + let isSkipped = interval.ruleCount === 0; + if (!isSkipped && effectiveEndTime) { + const intervalEnd = new Date(end); + isSkipped = intervalEnd > effectiveEndTime; + } + return (
    Date: Sun, 15 Feb 2026 18:57:07 -0600 Subject: [PATCH 58/90] Cleanup some ugly date parsing in supervisor timeline view --- .../reindexing-timeline.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index 6f4cd2b0ceeb..d37ded4237dc 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -18,6 +18,7 @@ import { Button, Callout, Card, Dialog, Intent, Tag, Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import { Duration, Timezone } from 'chronoshift'; import * as JSONBig from 'json-bigint-native'; import React, { useState } from 'react'; import AceEditor from 'react-ace'; @@ -151,22 +152,9 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( // Calculate effective end time if we have queried max time and skipOffsetFromLatest let effectiveEndTime: Date | undefined; if (queriedMaxTime && skipOffset?.notApplied) { - const maxTime = new Date(queriedMaxTime); const period = skipOffset.notApplied.period; - effectiveEndTime = new Date(maxTime); - - // Parse ISO 8601 duration format (e.g., "P7D", "P1M", "P1Y", "PT12H", "P1M7D") - const yearMatch = period.match(/(\d+)Y/); - const monthMatch = period.match(/(\d+)M/); - const weekMatch = period.match(/(\d+)W/); - const dayMatch = period.match(/(\d+)D/); - const hourMatch = period.match(/T.*?(\d+)H/); - - if (yearMatch) effectiveEndTime.setFullYear(effectiveEndTime.getFullYear() - parseInt(yearMatch[1], 10)); - if (monthMatch) effectiveEndTime.setMonth(effectiveEndTime.getMonth() - parseInt(monthMatch[1], 10)); - if (weekMatch) effectiveEndTime.setDate(effectiveEndTime.getDate() - parseInt(weekMatch[1], 10) * 7); - if (dayMatch) effectiveEndTime.setDate(effectiveEndTime.getDate() - parseInt(dayMatch[1], 10)); - if (hourMatch) effectiveEndTime.setHours(effectiveEndTime.getHours() - parseInt(hourMatch[1], 10)); + const duration = new Duration(period); + effectiveEndTime = duration.shift(new Date(queriedMaxTime), Timezone.UTC, -1); } // Display validation error if present From eb26cd1f9830158a9aa4b377ed91c8a993c24512 Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 15 Feb 2026 19:59:44 -0600 Subject: [PATCH 59/90] fix a rendering bug for the UI component --- .../reindexing-timeline.tsx | 171 ++++++++++++------ 1 file changed, 116 insertions(+), 55 deletions(-) diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index d37ded4237dc..a6612e8d49da 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -19,6 +19,7 @@ import { Button, Callout, Card, Dialog, Intent, Tag, Tooltip } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; import { Duration, Timezone } from 'chronoshift'; +import { format as formatDate } from 'date-fns'; import * as JSONBig from 'json-bigint-native'; import React, { useState } from 'react'; import AceEditor from 'react-ace'; @@ -40,15 +41,58 @@ const TIMELINE_INTERVAL_COLORS = [ '#2F343C', // darker gray ]; +const SKIPPED_INTERVAL_COLOR = 'rgba(219, 55, 55, 0.15)'; +const JSON_VIEWER_HEIGHT = '500px'; + +function getIntervalColor(index: number): string { + return TIMELINE_INTERVAL_COLORS[index % TIMELINE_INTERVAL_COLORS.length]; +} + interface ReindexingTimelineProps { supervisorId: string; } +interface DimFilter { + type: string; + field?: DimFilter; + fields?: DimFilter[]; +} + +interface TransformSpec { + filter?: DimFilter; + virtualColumns?: any; +} + +interface GranularitySpec { + segmentGranularity?: string; + queryGranularity?: string; + rollup?: boolean; +} + +interface CompactionConfig { + granularitySpec?: GranularitySpec; + metricsSpec?: any[]; + dimensionsSpec?: { + dimensions?: any[]; + }; + projections?: any[]; + transformSpec?: TransformSpec; + tuningConfig?: any; + ioConfig?: any; +} + +interface ReindexingRule { + type: string; + id?: string; + olderThan?: string; + [key: string]: any; +} + interface IntervalConfig { interval: string; ruleCount: number; - config: any; - appliedRules: any[]; + config: CompactionConfig; + appliedRules: ReindexingRule[]; } interface SkipOffsetInfo { @@ -153,8 +197,16 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( let effectiveEndTime: Date | undefined; if (queriedMaxTime && skipOffset?.notApplied) { const period = skipOffset.notApplied.period; - const duration = new Duration(period); - effectiveEndTime = duration.shift(new Date(queriedMaxTime), Timezone.UTC, -1); + try { + const duration = new Duration(period); + effectiveEndTime = duration.shift(new Date(queriedMaxTime), Timezone.UTC, -1); + } catch (e) { + console.error('Failed to parse skip offset period:', period, e); + AppToaster.show({ + message: `Invalid skip offset period format: ${period}`, + intent: Intent.WARNING, + }); + } } // Display validation error if present @@ -208,11 +260,6 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( const selectedInterval = selectedIntervalIndex !== undefined ? intervals[selectedIntervalIndex] : undefined; - // Generate a color based on interval index using blue/gray scale theme - const getIntervalColor = (index: number) => { - return TIMELINE_INTERVAL_COLORS[index % TIMELINE_INTERVAL_COLORS.length]; - }; - return (
    @@ -227,7 +274,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( Reference Time: {' '} - {new Date(referenceTime).toLocaleString()} + {formatDateTimeUTC(referenceTime)}
    {skipOffset && (
    @@ -263,7 +310,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( )} {skipOffset.notApplied && queriedMaxTime && effectiveEndTime && ( - {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Applied (latest: {new Date(queriedMaxTime).toISOString()}) + {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Applied (latest: {formatDateTimeUTC(queriedMaxTime)}) )}
    @@ -271,7 +318,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline(
    -
    +
    {intervals.map((interval, idx) => { const [start, end] = interval.interval.split('/'); const isSelected = selectedIntervalIndex === idx; @@ -288,12 +335,26 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( key={idx} className={`timeline-segment ${isSelected ? 'selected' : ''} ${isSkipped ? 'skipped' : ''}`} style={{ - backgroundColor: isSkipped ? 'rgba(219, 55, 55, 0.15)' : getIntervalColor(idx), + backgroundColor: isSkipped ? SKIPPED_INTERVAL_COLOR : getIntervalColor(idx), flex: 1, opacity: isSelected ? 1 : 0.7, cursor: isSkipped ? 'default' : 'pointer', }} + role={isSkipped ? undefined : 'button'} + tabIndex={isSkipped ? undefined : 0} + aria-label={ + isSkipped + ? `Skipped interval ${interval.interval}` + : `Interval ${interval.interval} with ${interval.ruleCount} rule${interval.ruleCount !== 1 ? 's' : ''}` + } + aria-selected={isSelected} onClick={() => !isSkipped && setSelectedIntervalIndex(idx)} + onKeyDown={(e) => { + if (!isSkipped && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + setSelectedIntervalIndex(idx); + } + }} title={isSkipped ? `${interval.interval}\nSkipped (beyond skip offset)` : `${interval.interval}\n${interval.ruleCount} rule(s) applied`} @@ -332,12 +393,18 @@ function formatDateShort(isoDate: string): string { return '-INF'; } - const date = new Date(isoDate); + // Format: "Feb 15, 2026" + return formatDate(new Date(isoDate), 'MMM d, yyyy'); +} - const month = date.toLocaleString('default', { month: 'short', timeZone: 'UTC' }); - const day = date.getUTCDate(); - const year = date.getUTCFullYear(); - return `${month} ${day}, ${year}`; +function formatDateTimeUTC(isoDate: string): string { + // Handle start of time / very old dates + if (isoDate.startsWith('-')) { + return '-INF'; + } + + // Format: "Feb 15, 2026 3:45 PM UTC" + return formatDate(new Date(isoDate), "MMM d, yyyy h:mm a 'UTC'"); } function formatInterval(interval: string): string { @@ -347,6 +414,20 @@ function formatInterval(interval: string): string { return `${formattedStart}/${formattedEnd}`; } +function handleCopyToClipboard(data: CompactionConfig | ReindexingRule[], label: string): void { + const jsonValue = JSONBig.stringify(data, undefined, 2); + navigator.clipboard.writeText(jsonValue); + AppToaster.show({ + message: `${label} copied to clipboard`, + intent: Intent.SUCCESS, + }); +} + +function handleDownloadJson(data: CompactionConfig | ReindexingRule[], filename: string): void { + const jsonValue = JSONBig.stringify(data, undefined, 2); + downloadFile(jsonValue, 'json', filename); +} + interface IntervalDetailPanelProps { interval: IntervalConfig; onClose: () => void; @@ -373,7 +454,12 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { {interval.ruleCount} rule{interval.ruleCount !== 1 ? 's' : ''} applied
    -
    @@ -447,24 +533,13 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { text="Copy" icon={IconNames.DUPLICATE} intent={Intent.PRIMARY} - onClick={() => { - const jsonValue = JSONBig.stringify(config, undefined, 2); - navigator.clipboard.writeText(jsonValue); - AppToaster.show({ - message: 'Configuration copied to clipboard', - intent: Intent.SUCCESS, - }); - }} + onClick={() => handleCopyToClipboard(config, 'Configuration')} />
    @@ -490,24 +565,13 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { text="Copy" icon={IconNames.DUPLICATE} intent={Intent.PRIMARY} - onClick={() => { - const jsonValue = JSONBig.stringify(interval.appliedRules, undefined, 2); - navigator.clipboard.writeText(jsonValue); - AppToaster.show({ - message: 'Rules copied to clipboard', - intent: Intent.SUCCESS, - }); - }} + onClick={() => handleCopyToClipboard(interval.appliedRules, 'Rules')} />
    @@ -516,7 +580,7 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { ); } -function countDeletionRules(transformSpec: any): number { +function countDeletionRules(transformSpec?: TransformSpec): number { if (!transformSpec || !transformSpec.filter) { return 0; } @@ -537,28 +601,25 @@ function countDeletionRules(transformSpec: any): number { } interface ConfigJsonViewerProps { - config: any; + config: CompactionConfig | ReindexingRule[]; isRulesList?: boolean; } function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { - let displayValue: any; let jsonValue: string; if (isRulesList && Array.isArray(config)) { // Group rules by type for better readability - const rulesByType: Record = {}; - config.forEach((rule: any) => { + const rulesByType: Record = {}; + config.forEach((rule: ReindexingRule) => { const type = getRuleTypeName(rule); if (!rulesByType[type]) { rulesByType[type] = []; } rulesByType[type].push(rule); }); - displayValue = rulesByType; - jsonValue = JSONBig.stringify(displayValue, undefined, 2); + jsonValue = JSONBig.stringify(rulesByType, undefined, 2); } else { - displayValue = config; jsonValue = JSONBig.stringify(config, undefined, 2); } @@ -571,7 +632,7 @@ function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { value={jsonValue} readOnly width="100%" - height="500px" + height={JSON_VIEWER_HEIGHT} showPrintMargin={false} showGutter editorProps={{ $blockScrolling: Infinity }} @@ -584,7 +645,7 @@ function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { ); } -function getRuleTypeName(rule: any): string { +function getRuleTypeName(rule: ReindexingRule): string { // Use the explicit type field provided by Jackson serialization const typeMap: Record = { 'deletion': 'Deletion Rules', From 66d681528a8b63b0e8381c6b3957aa53c548560c Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 15 Feb 2026 21:09:59 -0600 Subject: [PATCH 60/90] web console testing and fixes found while writing tests --- web-console/src/components/index.ts | 2 +- .../reindexing-timeline.spec.tsx.snap | 694 ++++++++++++++++++ .../reindexing-timeline.scss | 2 +- .../reindexing-timeline.spec.tsx | 460 ++++++++++++ .../reindexing-timeline.tsx | 127 ++-- ...pervisor-table-action-dialog.spec.tsx.snap | 27 + 6 files changed, 1262 insertions(+), 50 deletions(-) create mode 100644 web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap create mode 100644 web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts index 75fd22a5b3a2..bf28a4c501b7 100644 --- a/web-console/src/components/index.ts +++ b/web-console/src/components/index.ts @@ -25,7 +25,6 @@ export * from './braced-text/braced-text'; export * from './center-message/center-message'; export * from './clearable-input/clearable-input'; export * from './click-to-copy/click-to-copy'; -export * from './reindexing-timeline/reindexing-timeline'; export * from './deferred/deferred'; export * from './druid-logo/druid-logo'; export * from './external-link/external-link'; @@ -49,6 +48,7 @@ export * from './portal-bubble/portal-bubble'; export * from './query-error-pane/query-error-pane'; export * from './record-table-pane/record-table-pane'; export * from './refresh-button/refresh-button'; +export * from './reindexing-timeline/reindexing-timeline'; export * from './rule-editor/rule-editor'; export * from './segment-timeline/segment-timeline'; export * from './show-json/show-json'; diff --git a/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap b/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap new file mode 100644 index 000000000000..25db7747dc8e --- /dev/null +++ b/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap @@ -0,0 +1,694 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReindexingTimeline component rendering matches snapshot for empty intervals 1`] = ` +
    +
    + + No reindexing intervals found. This may indicate that the rule provider is not ready or no rules are configured. +
    +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot for error state 1`] = ` +
    +
    + +
    + Error loading reindexing timeline +
    + Failed to load timeline +
    +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot for loading state 1`] = ` +
    + +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot with normal intervals 1`] = ` +
    +
    +
    + + DataSource: + + + test-datasource + + | + + + + Reference Time: + + + + Nov 15, 2024 12:00 PM UTC +
    +
    +
    + +
    + Click on an interval to view its configuration details +
    +
    +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot with skip offset applied 1`] = ` +
    +
    +
    + + DataSource: + + + test-datasource + + | + + + + Reference Time: + + + + Nov 15, 2024 12:00 PM UTC +
    +
    + + + + Skip Offset: + skipOffsetFromNow + ( + P7D + ) + + +
    +
    +
    + +
    + Click on an interval to view its configuration details +
    +
    +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot with skip offset not applied 1`] = ` +
    +
    +
    + + DataSource: + + + test-datasource + + | + + + + Reference Time: + + + + Nov 15, 2024 12:00 PM UTC +
    +
    + + + + + skipOffsetFromLatest + ( + P7D + ): Not reflected in this preview + + + + +
    +
    +
    + +
    + Click on an interval to view its configuration details +
    +
    +
    +`; + +exports[`ReindexingTimeline component rendering matches snapshot with validation error 1`] = ` +
    +
    + +
    + Invalid Supervisor Configuration +
    +

    + + The segment granularity rule definitions have created an illegal segment granularity timeline. + +

    +

    + Segment granularity must stay the same or become + + coarser + + as data ages from present to past. Your configuration violates this constraint: +

    +
      +
    • + + Older interval + + (further in the past): + + + 2024-01-01/2024-02-01 + + has + + + finer + + granularity: + + + + HOUR + + +
    • +
    • + + Newer interval + + (more recent): + + + 2024-02-01/2024-03-01 + + has + + + coarser + + granularity: + + + + DAY + + +
    • +
    +

    + + To fix this: + + Adjust your segment granularity rules so that as time moves from present to past, the granularity either stays the same or gets coarser (e.g., HOUR → DAY → MONTH → YEAR). +

    +
    +
    +`; diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.scss b/web-console/src/components/reindexing-timeline/reindexing-timeline.scss index 9581d0173880..92882ceb0f87 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.scss +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.scss @@ -199,4 +199,4 @@ font-size: 12px; } } -} \ No newline at end of file +} diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx new file mode 100644 index 000000000000..315b7b1dc7c0 --- /dev/null +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx @@ -0,0 +1,460 @@ +/* + * 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. + */ + +import { fireEvent, render } from '@testing-library/react'; + +import type { useQueryManager as UseQueryManager } from '../../hooks'; +import * as hooks from '../../hooks'; +import * as singletons from '../../singletons'; +import { QueryState } from '../../utils'; + +import { ReindexingTimeline } from './reindexing-timeline'; + +// Mock the useQueryManager hook +jest.mock('../../hooks', () => ({ + useQueryManager: jest.fn(), +})); + +const mockUseQueryManager = hooks.useQueryManager as jest.MockedFunction; + +// Mock Api singleton +jest.mock('../../singletons', () => ({ + Api: { + instance: { + get: jest.fn(), + post: jest.fn(), + encodePath: jest.fn((path: string) => path), + }, + }, + AppToaster: { + show: jest.fn(), + }, +})); + +// Get mock references after mocking +const mockPost = singletons.Api.instance.post as jest.Mock; +const mockAppToasterShow = singletons.AppToaster.show as jest.Mock; + +// Mock chronoshift Duration +jest.mock('chronoshift', () => ({ + Duration: jest.fn().mockImplementation((period: string) => { + if (period === 'INVALID') { + throw new Error('Invalid period format'); + } + return { + shift: jest.fn((date: Date) => new Date(date.getTime() - 7 * 24 * 60 * 60 * 1000)), + }; + }), + Timezone: { + UTC: 'UTC', + }, +})); + +// Sample test data +const mockTimelineData = { + dataSource: 'test-datasource', + referenceTime: '2024-11-15T12:00:00.000Z', + intervals: [ + { + interval: '2024-11-01T00:00:00.000Z/2024-11-08T00:00:00.000Z', + ruleCount: 3, + config: { + granularitySpec: { + segmentGranularity: 'DAY', + queryGranularity: 'HOUR', + rollup: true, + }, + metricsSpec: [{ type: 'count', name: 'count' }], + }, + appliedRules: [ + { type: 'dataSchema', id: 'schema-1' }, + { type: 'segmentGranularity', id: 'gran-1' }, + { type: 'tuningConfig', id: 'tuning-1' }, + ], + }, + { + interval: '2024-11-08T00:00:00.000Z/2024-11-15T00:00:00.000Z', + ruleCount: 0, + config: {}, + appliedRules: [], + }, + ], +}; + +describe('ReindexingTimeline', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ==================================================================== + // PHASE 1: UTILITY FUNCTIONS + // ==================================================================== + describe('utility functions', () => { + // Note: These are internal functions, so we'll test them indirectly through component behavior + // For direct testing, we'd need to export them, which may not be desired + + describe('date formatting', () => { + it('formats dates consistently across the component', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ data: mockTimelineData }), + {} as any, + ]); + + const { container } = render(); + + // Reference time should be formatted with UTC + expect(container.textContent).toContain('Nov 15, 2024'); + }); + }); + + describe('interval color generation', () => { + it('applies different colors to different intervals', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ data: mockTimelineData }), + {} as any, + ]); + + const { container } = render(); + + const segments = container.querySelectorAll('.timeline-segment'); + expect(segments.length).toBe(2); + + // Each segment should have a background color + segments.forEach(segment => { + const htmlSegment = segment as HTMLElement; + expect(htmlSegment.style.backgroundColor).toBeTruthy(); + }); + }); + }); + }); + + // ==================================================================== + // PHASE 2: COMPONENT RENDERING + // ==================================================================== + describe('component rendering', () => { + it('matches snapshot for loading state', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ loading: true }), {} as any]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot for error state', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ error: new Error('Failed to load timeline') }), + {} as any, + ]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot for empty intervals', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + intervals: [], + }, + }), + {} as any, + ]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with validation error', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + intervals: [], + validationError: { + errorType: 'INVALID_GRANULARITY_TIMELINE', + message: 'Granularity must become coarser over time', + olderInterval: '2024-01-01/2024-02-01', + olderGranularity: 'HOUR', + newerInterval: '2024-02-01/2024-03-01', + newerGranularity: 'DAY', + }, + }, + }), + {} as any, + ]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with normal intervals', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with skip offset applied', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + applied: { + type: 'skipOffsetFromNow', + period: 'P7D', + effectiveEndTime: '2024-11-08T12:00:00.000Z', + }, + }, + }, + }), + {} as any, + ]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('matches snapshot with skip offset not applied', () => { + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + notApplied: { + type: 'skipOffsetFromLatest', + period: 'P7D', + reason: 'Requires actual segment timeline data', + }, + }, + }, + }), + {} as any, + ]); + + const { container } = render(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('marks skipped intervals with special styling', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + + const segments = container.querySelectorAll('.timeline-segment'); + const skippedSegments = container.querySelectorAll('.timeline-segment.skipped'); + + expect(segments.length).toBe(2); + expect(skippedSegments.length).toBe(1); // Second interval has ruleCount: 0 + }); + + it('displays interval details when interval is selected', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + + // Initially no detail panel + expect(container.querySelector('.interval-detail-panel')).toBeNull(); + + // Click first interval + const firstSegment = container.querySelector('.timeline-segment:not(.skipped)')!; + fireEvent.click(firstSegment); + + // Detail panel should appear + expect(container.querySelector('.interval-detail-panel')).toBeTruthy(); + }); + }); + + // ==================================================================== + // PHASE 3: ERROR HANDLING + // ==================================================================== + describe('error handling', () => { + it('handles invalid Duration period gracefully', () => { + const mockDuration = jest.requireMock('chronoshift').Duration; + mockDuration.mockImplementationOnce(() => { + throw new Error('Invalid ISO 8601 duration'); + }); + + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + notApplied: { + type: 'skipOffsetFromLatest', + period: 'INVALID', + reason: 'Test', + }, + }, + }, + }), + {} as any, + ]); + + // Should not crash + const { container } = render(); + expect(container).toBeTruthy(); + }); + + it('handles API errors in handleQueryMaxTime', async () => { + mockPost.mockRejectedValueOnce(new Error('Network error')); + + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + notApplied: { + type: 'skipOffsetFromLatest', + period: 'P7D', + reason: 'Test', + }, + }, + }, + }), + {} as any, + ]); + + const { getByText } = render(); + + const button = getByText('Query latest timestamp'); + fireEvent.click(button); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 100)); + + // Should show error toast + expect(mockAppToasterShow).toHaveBeenCalledWith( + expect.objectContaining({ + intent: 'danger', + }), + ); + }); + + it('handles empty response from timeBoundary query', async () => { + mockPost.mockResolvedValueOnce({ data: [] }); + + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + notApplied: { + type: 'skipOffsetFromLatest', + period: 'P7D', + reason: 'Test', + }, + }, + }, + }), + {} as any, + ]); + + const { getByText } = render(); + + const button = getByText('Query latest timestamp'); + fireEvent.click(button); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 100)); + + // Should show warning toast + expect(mockAppToasterShow).toHaveBeenCalledWith( + expect.objectContaining({ + intent: 'warning', + message: 'No data found in datasource', + }), + ); + }); + + it('handles missing result in timeBoundary response', async () => { + mockPost.mockResolvedValueOnce({ + data: [{ result: null }], + }); + + mockUseQueryManager.mockReturnValue([ + new QueryState({ + data: { + ...mockTimelineData, + skipOffset: { + notApplied: { + type: 'skipOffsetFromLatest', + period: 'P7D', + reason: 'Test', + }, + }, + }, + }), + {} as any, + ]); + + const { getByText } = render(); + + const button = getByText('Query latest timestamp'); + fireEvent.click(button); + + // Wait for async operation + await new Promise(resolve => setTimeout(resolve, 100)); + + // Should show warning toast + expect(mockAppToasterShow).toHaveBeenCalledWith( + expect.objectContaining({ + intent: 'warning', + }), + ); + }); + }); + + // ==================================================================== + // ACCESSIBILITY + // ==================================================================== + describe('accessibility', () => { + it('has proper ARIA attributes on interactive intervals', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + + const interactiveSegment = container.querySelector('.timeline-segment:not(.skipped)')!; + + expect(interactiveSegment.getAttribute('role')).toBe('button'); + expect(interactiveSegment.getAttribute('tabindex')).toBe('0'); + expect(interactiveSegment.getAttribute('aria-label')).toBeTruthy(); + }); + + it('does not make skipped intervals interactive', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + + const skippedSegment = container.querySelector('.timeline-segment.skipped')!; + + expect(skippedSegment.getAttribute('role')).toBeNull(); + expect(skippedSegment.getAttribute('tabindex')).toBeNull(); + }); + + it('has toolbar role on timeline container', () => { + mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); + + const { container } = render(); + + const timeline = container.querySelector('.timeline-bar'); + expect(timeline?.getAttribute('role')).toBe('toolbar'); + expect(timeline?.getAttribute('aria-label')).toBeTruthy(); + }); + }); +}); diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx index a6612e8d49da..dd0ad357d837 100644 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx @@ -215,30 +215,39 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline(

    - The segment granularity rule definitions have created an illegal segment granularity timeline. + + The segment granularity rule definitions have created an illegal segment granularity + timeline. +

    - {validationError.errorType === 'INVALID_GRANULARITY_TIMELINE' && validationError.olderInterval && ( - <> -

    - Segment granularity must stay the same or become coarser as data ages from present to past. - Your configuration violates this constraint: -

    -
      -
    • - Older interval (further in the past): {formatInterval(validationError.olderInterval)} - {' '}has finer granularity: {validationError.olderGranularity} -
    • -
    • - Newer interval (more recent): {formatInterval(validationError.newerInterval!)} - {' '}has coarser granularity: {validationError.newerGranularity} -
    • -
    -

    - To fix this: Adjust your segment granularity rules so that as time moves from present to past, - the granularity either stays the same or gets coarser (e.g., HOUR → DAY → MONTH → YEAR). -

    - - )} + {validationError.errorType === 'INVALID_GRANULARITY_TIMELINE' && + validationError.olderInterval && ( + <> +

    + Segment granularity must stay the same or become coarser as data + ages from present to past. Your configuration violates this constraint: +

    +
      +
    • + Older interval (further in the past):{' '} + {formatInterval(validationError.olderInterval)} has{' '} + finer granularity:{' '} + {validationError.olderGranularity} +
    • +
    • + Newer interval (more recent):{' '} + {formatInterval(validationError.newerInterval!)} has{' '} + coarser granularity:{' '} + {validationError.newerGranularity} +
    • +
    +

    + To fix this: Adjust your segment granularity rules so that as + time moves from present to past, the granularity either stays the same or gets + coarser (e.g., HOUR → DAY → MONTH → YEAR). +

    + + )} {validationError.errorType !== 'INVALID_GRANULARITY_TIMELINE' && (

    {validationError.message}

    )} @@ -251,14 +260,15 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( return (
    - No reindexing intervals found. This may indicate that the rule provider is not ready or - no rules are configured. + No reindexing intervals found. This may indicate that the rule provider is not ready or no + rules are configured.
    ); } - const selectedInterval = selectedIntervalIndex !== undefined ? intervals[selectedIntervalIndex] : undefined; + const selectedInterval = + selectedIntervalIndex !== undefined ? intervals[selectedIntervalIndex] : undefined; return (
    @@ -295,14 +305,15 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( position="bottom" > - {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in this preview + {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in + this preview
    @@ -333,7 +345,9 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( return (
    !isSkipped && setSelectedIntervalIndex(idx)} - onKeyDown={(e) => { + onKeyDown={e => { if (!isSkipped && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); setSelectedIntervalIndex(idx); } }} - title={isSkipped - ? `${interval.interval}\nSkipped (beyond skip offset)` - : `${interval.interval}\n${interval.ruleCount} rule(s) applied`} + title={ + isSkipped + ? `${interval.interval}\nSkipped (beyond skip offset)` + : `${interval.interval}\n${interval.ruleCount} rule(s) applied` + } >
    {formatDateShort(start)}
    {formatDateShort(end)}
    - {isSkipped ? 'skipped' : `${interval.ruleCount} rule${interval.ruleCount !== 1 ? 's' : ''}`} + {isSkipped + ? 'skipped' + : `${interval.ruleCount} rule${interval.ruleCount !== 1 ? 's' : ''}`}
    @@ -372,9 +392,7 @@ export const ReindexingTimeline = React.memo(function ReindexingTimeline( ); })}
    -
    - Click on an interval to view its configuration details -
    +
    Click on an interval to view its configuration details
    {selectedInterval && selectedInterval.ruleCount > 0 && ( @@ -416,7 +434,7 @@ function formatInterval(interval: string): string { function handleCopyToClipboard(data: CompactionConfig | ReindexingRule[], label: string): void { const jsonValue = JSONBig.stringify(data, undefined, 2); - navigator.clipboard.writeText(jsonValue); + void navigator.clipboard.writeText(jsonValue); AppToaster.show({ message: `${label} copied to clipboard`, intent: Intent.SUCCESS, @@ -539,7 +557,12 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { text="Download" icon={IconNames.DOWNLOAD} intent={Intent.PRIMARY} - onClick={() => handleDownloadJson(config, `reindexing-config-${interval.interval.replace(/\//g, '-')}.json`)} + onClick={() => + handleDownloadJson( + config, + `reindexing-config-${interval.interval.replace(/\//g, '-')}.json`, + ) + } />
    @@ -549,7 +572,10 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { isOpen={showRawRules} onClose={() => setShowRawRules(false)} title={ - + Rules for {formatInterval(interval.interval)} } @@ -571,7 +597,12 @@ function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { text="Download" icon={IconNames.DOWNLOAD} intent={Intent.PRIMARY} - onClick={() => handleDownloadJson(interval.appliedRules, `reindexing-rules-${interval.interval.replace(/\//g, '-')}.json`)} + onClick={() => + handleDownloadJson( + interval.appliedRules, + `reindexing-rules-${interval.interval.replace(/\//g, '-')}.json`, + ) + } />
    @@ -648,11 +679,11 @@ function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { function getRuleTypeName(rule: ReindexingRule): string { // Use the explicit type field provided by Jackson serialization const typeMap: Record = { - 'deletion': 'Deletion Rules', - 'dataSchema': 'Data Schema Rules', - 'segmentGranularity': 'Segment Granularity Rules', - 'tuningConfig': 'Tuning Config Rules', - 'ioConfig': 'IO Config Rules', + deletion: 'Deletion Rules', + dataSchema: 'Data Schema Rules', + segmentGranularity: 'Segment Granularity Rules', + tuningConfig: 'Tuning Config Rules', + ioConfig: 'IO Config Rules', }; return typeMap[rule.type] || 'Other Rules'; diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap index ee93c8fd6222..f0bede858f21 100755 --- a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap +++ b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap @@ -174,6 +174,33 @@ exports[`SupervisorTableActionDialog matches snapshot 1`] = ` History +
    Date: Sun, 15 Feb 2026 21:41:03 -0600 Subject: [PATCH 61/90] clean refactor of reindexing config builder --- .../compaction/ReindexingConfigBuilder.java | 18 ++---------------- .../ReindexingConfigBuilderTest.java | 3 --- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java index 7820b7c8897e..5a88ae1369f1 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java @@ -20,7 +20,6 @@ package org.apache.druid.server.compaction; import org.apache.druid.error.DruidException; -import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.query.filter.NotDimFilter; @@ -36,8 +35,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; /** * Builds compaction configs by applying reindexing rules. @@ -48,7 +45,6 @@ public class ReindexingConfigBuilder private static final Logger LOG = new Logger(ReindexingConfigBuilder.class); private final ReindexingRuleProvider provider; - private final Granularity defaultGranularity; private final Interval interval; private final DateTime referenceTime; private final List syntheticTimeline; @@ -91,14 +87,12 @@ public List getAppliedRules() public ReindexingConfigBuilder( ReindexingRuleProvider provider, - Granularity defaultSegmentGranularity, Interval interval, DateTime referenceTime, - @Nullable List syntheticTimeline + List syntheticTimeline ) { this.provider = provider; - this.defaultGranularity = defaultSegmentGranularity; this.interval = interval; this.referenceTime = referenceTime; this.syntheticTimeline = syntheticTimeline; @@ -174,11 +168,7 @@ public BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Bui count++; } - if (count == 0) { - return new BuildResult(0, List.of()); - } else { - return new BuildResult(count, appliedRules); - } + return new BuildResult(count, count == 0 ? List.of() : appliedRules); } /** @@ -188,10 +178,6 @@ public BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Bui @Nullable private IntervalGranularityInfo findMatchingInterval(Interval interval) { - if (syntheticTimeline == null) { - return null; - } - for (IntervalGranularityInfo candidate : syntheticTimeline) { if (candidate.getInterval().equals(interval)) { return candidate; diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java index 544d76febd09..62a7ca8e9f5f 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java @@ -75,7 +75,6 @@ public void test_applyTo_handlesSynteticSegmentGranularityInsertion() ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, - Granularities.DAY, TEST_INTERVAL, REFERENCE_TIME, syntheticTimeline @@ -137,7 +136,6 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, - Granularities.DAY, TEST_INTERVAL, REFERENCE_TIME, syntheticTimeline @@ -220,7 +218,6 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( provider, - Granularities.DAY, TEST_INTERVAL, REFERENCE_TIME, syntheticTimeline From a39f2225f59ef0975b88cff7a15eac193e48ca12 Mon Sep 17 00:00:00 2001 From: capistrant Date: Sun, 15 Feb 2026 21:41:18 -0600 Subject: [PATCH 62/90] clean up refactor of cascading reindexing template --- .../compact/CascadingReindexingTemplate.java | 320 +++++++++++------- .../CascadingReindexingTemplateTest.java | 20 +- 2 files changed, 222 insertions(+), 118 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index fcb7714394ba..92941a446c55 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -130,16 +130,8 @@ public CascadingReindexingTemplate( if (skipOffsetFromNow != null && skipOffsetFromLatest != null) { throw new IAE("Cannot set both skipOffsetFromNow and skipOffsetFromLatest"); } - if (skipOffsetFromLatest != null) { - this.skipOffsetFromLatest = skipOffsetFromLatest; - this.skipOffsetFromNow = null; - } else if (skipOffsetFromNow != null) { - this.skipOffsetFromNow = skipOffsetFromNow; - this.skipOffsetFromLatest = null; - } else { - this.skipOffsetFromLatest = null; - this.skipOffsetFromNow = null; - } + this.skipOffsetFromNow = skipOffsetFromNow; + this.skipOffsetFromLatest = skipOffsetFromLatest; } @Override @@ -406,10 +398,9 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( ruleProvider, - defaultSegmentGranularity, searchInterval, referenceTime, - searchIntervals // Pass synthetic timeline + searchIntervals ); ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builder); @@ -491,10 +482,9 @@ public List createCompactionJobs( ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( ruleProvider, - defaultSegmentGranularity, reindexingInterval, currentTime, - searchIntervals // Pass synthetic timeline + searchIntervals ); int ruleCount = configBuilder.applyTo(builder); @@ -559,7 +549,7 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() .withInputSegmentSizeBytes(inputSegmentSizeBytes) .withEngine(engine) .withTaskContext(taskContext) - .withSkipOffsetFromLatest(Period.ZERO); + .withSkipOffsetFromLatest(Period.ZERO); // We handle skip offsets at the timeline level, we know we want to cover the entirety of the interval } /** @@ -581,63 +571,102 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() * * @param referenceTime the reference time for calculating period thresholds * @return list of split and aligned intervals with their granularities and source rules, ordered from oldest to newest - * @throws IAE if no segment granularity rules are found + * @throws IAE if no reindexing rules are configured + * @throws GranularityTimelineValidationException if granularities become coarser over time */ List generateAlignedSearchIntervals(DateTime referenceTime) { List baseTimeline = generateBaseSegmentGranularityAlignedTimeline(referenceTime); - List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); List finalIntervals = new ArrayList<>(); - for (IntervalGranularityInfo baseInterval : baseTimeline) { - List splitPoints = new ArrayList<>(); + List splitPoints = findGranularityAlignedSplitPoints(baseInterval, nonSegmentGranThresholds); + finalIntervals.addAll(splitIntervalAtPoints(baseInterval, splitPoints)); + } - for (DateTime threshold : nonSegmentGranThresholds) { - if (threshold.isAfter(baseInterval.getInterval().getStart()) && - threshold.isBefore(baseInterval.getInterval().getEnd())) { + return finalIntervals; + } - // Align threshold to this interval's segment granularity - DateTime alignedThreshold = baseInterval.getGranularity().bucketStart(threshold); + /** + * Finds split points within a base interval by aligning non-segment-granularity thresholds + * to the interval's segment granularity. Only includes thresholds that fall strictly inside + * the interval (not at boundaries, which would create zero-length intervals). + * + * @param baseInterval the interval to find split points for + * @param nonSegmentGranThresholds thresholds from non-segment-granularity rules + * @return sorted, distinct list of aligned split points that fall inside the interval + */ + private List findGranularityAlignedSplitPoints( + IntervalGranularityInfo baseInterval, + List nonSegmentGranThresholds + ) + { + List splitPoints = new ArrayList<>(); - // Only add if it's not at the boundaries (would create zero-length interval) - if (alignedThreshold.isAfter(baseInterval.getInterval().getStart()) && - alignedThreshold.isBefore(baseInterval.getInterval().getEnd())) { - splitPoints.add(alignedThreshold); - } - } - } + for (DateTime threshold : nonSegmentGranThresholds) { + // Check if threshold falls inside this interval + if (threshold.isAfter(baseInterval.getInterval().getStart()) && + threshold.isBefore(baseInterval.getInterval().getEnd())) { - splitPoints = splitPoints.stream() - .distinct() - .sorted() - .collect(Collectors.toList()); + // Align threshold to this interval's segment granularity + DateTime alignedThreshold = baseInterval.getGranularity().bucketStart(threshold); - // Split this base interval at the split points, preserving granularity and source rule - if (splitPoints.isEmpty()) { - LOG.debug("No splits for interval [%s]", baseInterval.getInterval()); - finalIntervals.add(baseInterval); - } else { - LOG.debug("Splitting interval [%s] at [%d] points", baseInterval.getInterval(), splitPoints.size()); - DateTime start = baseInterval.getInterval().getStart(); - for (DateTime splitPoint : splitPoints) { - finalIntervals.add(new IntervalGranularityInfo( - new Interval(start, splitPoint), - baseInterval.getGranularity(), - baseInterval.getSourceRule() // Preserve source rule from base interval - )); - start = splitPoint; + // Only add if it's not at the boundaries (would create zero-length interval) + if (alignedThreshold.isAfter(baseInterval.getInterval().getStart()) && + alignedThreshold.isBefore(baseInterval.getInterval().getEnd())) { + splitPoints.add(alignedThreshold); } - finalIntervals.add(new IntervalGranularityInfo( - new Interval(start, baseInterval.getInterval().getEnd()), - baseInterval.getGranularity(), - baseInterval.getSourceRule() // Preserve source rule from base interval - )); } } - return finalIntervals; + // Remove duplicates and sort + return splitPoints.stream() + .distinct() + .sorted() + .collect(Collectors.toList()); + } + + /** + * Splits a base interval at the given split points, preserving the interval's granularity + * and source rule. If no split points exist, returns the original interval unchanged. + * + * @param baseInterval the interval to split + * @param splitPoints sorted list of points to split at (must be inside the interval) + * @return list of split intervals, or singleton list with original interval if no splits + */ + private List splitIntervalAtPoints( + IntervalGranularityInfo baseInterval, + List splitPoints + ) + { + if (splitPoints.isEmpty()) { + LOG.debug("No splits for interval [%s]", baseInterval.getInterval()); + return Collections.singletonList(baseInterval); + } + + LOG.debug("Splitting interval [%s] at [%d] points", baseInterval.getInterval(), splitPoints.size()); + + List result = new ArrayList<>(); + DateTime start = baseInterval.getInterval().getStart(); + + for (DateTime splitPoint : splitPoints) { + result.add(new IntervalGranularityInfo( + new Interval(start, splitPoint), + baseInterval.getGranularity(), + baseInterval.getSourceRule() // Preserve source rule from base interval + )); + start = splitPoint; + } + + // Add final interval from last split point to end + result.add(new IntervalGranularityInfo( + new Interval(start, baseInterval.getInterval().getEnd()), + baseInterval.getGranularity(), + baseInterval.getSourceRule() // Preserve source rule from base interval + )); + + return result; } /** @@ -664,41 +693,80 @@ List generateAlignedSearchIntervals(DateTime referenceT * * * + * + * @param referenceTime the reference time for calculating period thresholds + * @return base timeline with granularity-aligned intervals, ordered from oldest to newest + * @throws IAE if no reindexing rules are configured + * @throws GranularityTimelineValidationException if granularities become coarser over time */ private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) { List segmentGranRules = ruleProvider.getSegmentGranularityRules(); List nonSegmentGranThresholds = collectNonSegmentGranularityThresholds(referenceTime); + List baseTimeline; + if (segmentGranRules.isEmpty()) { - // No segment granularity rules - use default granularity with the smallest period from other rules - if (nonSegmentGranThresholds.isEmpty()) { - throw new IAE( - "CascadingReindexingTemplate requires at least one reindexing rule " - + "(segment granularity or other type)" - ); - } + baseTimeline = createDefaultGranularityTimeline(nonSegmentGranThresholds); + } else { + baseTimeline = createSegmentGranularityTimeline(segmentGranRules, referenceTime); + baseTimeline = maybePrependRecentInterval(baseTimeline, nonSegmentGranThresholds); + } - // Find the smallest period (most recent threshold = largest DateTime value) - DateTime mostRecentThreshold = Collections.max(nonSegmentGranThresholds); - DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentThreshold); + validateSegmentGranularityTimeline(baseTimeline); + return baseTimeline; + } - LOG.debug( - "No segment granularity rules found for cascading supervisor[%s]. Creating base interval with " - + "default granularity [%s] and threshold [%s] (aligned: [%s])", - dataSource, - defaultSegmentGranularity, - mostRecentThreshold, - alignedEnd + /** + * Creates a timeline using the default segment granularity when no segment granularity rules exist. + * Uses the most recent threshold from non-segment-granularity rules to determine the end boundary. + * + * @param nonSegmentGranThresholds thresholds from non-segment-granularity rules + * @return single-interval timeline from MIN to most recent threshold, aligned to default granularity + * @throws IAE if no non-segment-granularity rules exist either + */ + private List createDefaultGranularityTimeline(List nonSegmentGranThresholds) + { + if (nonSegmentGranThresholds.isEmpty()) { + throw new IAE( + "CascadingReindexingTemplate requires at least one reindexing rule " + + "(segment granularity or other type)" ); - - return Collections.singletonList(new IntervalGranularityInfo( - new Interval(DateTimes.MIN, alignedEnd), - defaultSegmentGranularity, - null // No source rule when using default granularity - )); } + // Find the smallest period (most recent threshold = largest DateTime value) + DateTime mostRecentThreshold = Collections.max(nonSegmentGranThresholds); + DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentThreshold); + + LOG.debug( + "No segment granularity rules found for cascading supervisor[%s]. Creating base interval with " + + "default granularity [%s] and threshold [%s] (aligned: [%s])", + dataSource, + defaultSegmentGranularity, + mostRecentThreshold, + alignedEnd + ); + + return Collections.singletonList(new IntervalGranularityInfo( + new Interval(DateTimes.MIN, alignedEnd), + defaultSegmentGranularity, + null // No source rule when using default granularity + )); + } + + /** + * Creates a timeline by processing segment granularity rules in chronological order (oldest to newest). + * Each rule defines an interval with its specific segment granularity, with boundaries aligned to that granularity. + * + * @param segmentGranRules segment granularity rules to process + * @param referenceTime reference time for computing rule thresholds + * @return timeline of intervals with their granularities, ordered from oldest to newest + */ + private List createSegmentGranularityTimeline( + List segmentGranRules, + DateTime referenceTime + ) + { // Sort rules by period from longest to shortest (oldest to most recent threshold) List sortedRules = segmentGranRules.stream() .sorted(Comparator.comparingLong(rule -> { @@ -734,47 +802,65 @@ private List generateBaseSegmentGranularityAlignedTimel previousAlignedEnd = alignedEnd; } - // Check if we need to prepend an interval for non-segment-gran rules that are more recent - // than the most recent segment gran rule - if (!nonSegmentGranThresholds.isEmpty()) { - DateTime mostRecentNonSegmentGranThreshold = Collections.max(nonSegmentGranThresholds); - DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).getInterval().getEnd(); - - if (mostRecentNonSegmentGranThreshold.isAfter(mostRecentSegmentGranEnd)) { - DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); - - if (alignedEnd.isBefore(mostRecentSegmentGranEnd) || alignedEnd.isEqual(mostRecentSegmentGranEnd)) { - LOG.debug( - "Most recent non-segment-gran threshold [%s] aligns to [%s], which is not after " - + "most recent segment granularity rule interval end [%s]. No prepended interval needed.", - mostRecentNonSegmentGranThreshold, - alignedEnd, - mostRecentSegmentGranEnd - ); - return baseTimeline; - } else { - LOG.debug( - "Most recent non-segment-gran threshold [%s] is after most recent segment gran interval end [%s]. " - + "Prepending interval with default granularity [%s] (aligned end: [%s])", - mostRecentNonSegmentGranThreshold, - mostRecentSegmentGranEnd, - defaultSegmentGranularity, - alignedEnd - ); - - baseTimeline.add(new IntervalGranularityInfo( - new Interval(mostRecentSegmentGranEnd, alignedEnd), - defaultSegmentGranularity, - null // No source rule when using default granularity - )); - } - } + return baseTimeline; + } + + /** + * Checks if non-segment-granularity rules have more recent thresholds than the most recent + * segment granularity rule, and if so, prepends an interval with the default granularity. + * This ensures that all rules (not just segment granularity rules) are represented in the timeline. + * + * @param baseTimeline existing timeline built from segment granularity rules + * @param nonSegmentGranThresholds thresholds from non-segment-granularity rules + * @return updated timeline with prepended interval if needed, otherwise original timeline + */ + private List maybePrependRecentInterval( + List baseTimeline, + List nonSegmentGranThresholds + ) + { + if (nonSegmentGranThresholds.isEmpty()) { + return baseTimeline; } - // Validate the completed timeline before returning - validateSegmentGranularityTimeline(baseTimeline); + DateTime mostRecentNonSegmentGranThreshold = Collections.max(nonSegmentGranThresholds); + DateTime mostRecentSegmentGranEnd = baseTimeline.get(baseTimeline.size() - 1).getInterval().getEnd(); - return baseTimeline; + if (!mostRecentNonSegmentGranThreshold.isAfter(mostRecentSegmentGranEnd)) { + return baseTimeline; + } + + DateTime alignedEnd = defaultSegmentGranularity.bucketStart(mostRecentNonSegmentGranThreshold); + + if (alignedEnd.isBefore(mostRecentSegmentGranEnd) || alignedEnd.isEqual(mostRecentSegmentGranEnd)) { + LOG.debug( + "Most recent non-segment-gran threshold [%s] aligns to [%s], which is not after " + + "most recent segment granularity rule interval end [%s]. No prepended interval needed.", + mostRecentNonSegmentGranThreshold, + alignedEnd, + mostRecentSegmentGranEnd + ); + return baseTimeline; + } + + LOG.debug( + "Most recent non-segment-gran threshold [%s] is after most recent segment gran interval end [%s]. " + + "Prepending interval with default granularity [%s] (aligned end: [%s])", + mostRecentNonSegmentGranThreshold, + mostRecentSegmentGranEnd, + defaultSegmentGranularity, + alignedEnd + ); + + // Create new list with prepended interval (don't modify original) + List updatedTimeline = new ArrayList<>(baseTimeline); + updatedTimeline.add(new IntervalGranularityInfo( + new Interval(mostRecentSegmentGranEnd, alignedEnd), + defaultSegmentGranularity, + null // No source rule when using default granularity + )); + + return updatedTimeline; } /** @@ -786,7 +872,7 @@ private List generateBaseSegmentGranularityAlignedTimel * which is typically undesirable and inefficient. * * @param timeline the completed base timeline with granularity information - * @throws IAE if granularity becomes coarser as we move toward present + * @throws GranularityTimelineValidationException if granularity becomes coarser as we move toward present */ private void validateSegmentGranularityTimeline(List timeline) { diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 3a6abaea0c08..3cb3b8223ce0 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -1371,7 +1371,25 @@ public void test_getReindexingTimelineView_comprehensive() null, Period.days(7), new UserCompactionTaskQueryTuningConfig( - null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null ) ); From 6c842a739257f1f2f93d714eb0b6d8c5d703a7e1 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 11:18:44 -0600 Subject: [PATCH 63/90] self review cleanup --- .../indexing/compact/CascadingReindexingTemplate.java | 9 ++++----- ...> SegmentGranularityTimelineValidationException.java} | 0 .../indexing/overlord/supervisor/SupervisorResource.java | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{GranularityTimelineValidationException.java => SegmentGranularityTimelineValidationException.java} (100%) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 92941a446c55..14b1b2a18953 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -384,17 +384,16 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) // Check if interval extends past skip offset if (intervalEndsAfter(searchInterval, effectiveEndTime)) { - // Include in timeline but mark as skipped (no rules applied) + // Include in timeline, but indicate skipped by applying no rules. intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( searchInterval, - 0, // ruleCount = 0 indicates no rules applied (skipped due to skip offset) - null, // no config - Collections.emptyList() // no applied rules + 0, + null, + Collections.emptyList() )); continue; } - // Process intervals within skip offset normally InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( ruleProvider, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java similarity index 100% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/GranularityTimelineValidationException.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 8c68e5891bae..51fe216d9aeb 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -394,7 +394,7 @@ public Response getReindexingTimeline( DateTime referenceTime; if (referenceTimeStr != null) { try { - referenceTime = DateTime.parse(referenceTimeStr); + referenceTime = DateTimes.of(referenceTimeStr); } catch (IllegalArgumentException e) { return Response.status(Response.Status.BAD_REQUEST) From 33fc482a8b3acdbf1e7bd45212e791d4780d61f7 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 12:09:16 -0600 Subject: [PATCH 64/90] add missing tests for supervisor resource api --- .../supervisor/SupervisorResourceTest.java | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java index 6c099a53b3a1..e43eb52092d4 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java @@ -26,6 +26,10 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.druid.audit.AuditManager; +import org.apache.druid.indexing.compact.CascadingReindexingTemplate; +import org.apache.druid.indexing.compact.CompactionJobTemplate; +import org.apache.druid.indexing.compact.CompactionSupervisorSpec; +import org.apache.druid.indexing.compact.ReindexingTimelineView; import org.apache.druid.indexing.overlord.DataSourceMetadata; import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator; import org.apache.druid.indexing.overlord.TaskMaster; @@ -1396,6 +1400,131 @@ public void test_handoffTaskGroups_returnsAccepted() verifyAll(); } + @Test + public void testGetReindexingTimeline_success() + { + CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); + CascadingReindexingTemplate template = EasyMock.createMock(CascadingReindexingTemplate.class); + ReindexingTimelineView timelineView = EasyMock.createMock(ReindexingTimelineView.class); + + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); + EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); + EasyMock.expect(template.getReindexingTimelineView(EasyMock.anyObject())).andReturn(timelineView); + EasyMock.replay(compactionSpec, template, timelineView); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("compaction-id", null); + + Assert.assertEquals(200, response.getStatus()); + Assert.assertEquals(timelineView, response.getEntity()); + verifyAll(); + EasyMock.verify(compactionSpec, template, timelineView); + } + + @Test + public void testGetReindexingTimeline_supervisorNotFound() + { + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("missing-id")).andReturn(Optional.absent()); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("missing-id", null); + + Assert.assertEquals(404, response.getStatus()); + verifyAll(); + } + + @Test + public void testGetReindexingTimeline_notCompactionSupervisor() + { + TestSupervisorSpec regularSpec = new TestSupervisorSpec("regular-id", null, null); + + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("regular-id")).andReturn(Optional.of(regularSpec)); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("regular-id", null); + + Assert.assertEquals(400, response.getStatus()); + Map entity = (Map) response.getEntity(); + Assert.assertTrue(entity.get("error").toString().contains("not a compaction supervisor")); + verifyAll(); + } + + @Test + public void testGetReindexingTimeline_notCascadingReindexingTemplate() + { + CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); + CompactionJobTemplate template = EasyMock.createMock(CompactionJobTemplate.class); + + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); + EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); + EasyMock.replay(compactionSpec, template); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("compaction-id", null); + + Assert.assertEquals(400, response.getStatus()); + Map entity = (Map) response.getEntity(); + Assert.assertTrue(entity.get("error").toString().contains("only available for cascading reindexing supervisors")); + verifyAll(); + EasyMock.verify(compactionSpec, template); + } + + @Test + public void testGetReindexingTimeline_withReferenceTime() + { + CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); + CascadingReindexingTemplate template = EasyMock.createMock(CascadingReindexingTemplate.class); + ReindexingTimelineView timelineView = EasyMock.createMock(ReindexingTimelineView.class); + + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); + EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); + EasyMock.expect(template.getReindexingTimelineView(EasyMock.anyObject())).andReturn(timelineView); + EasyMock.replay(compactionSpec, template, timelineView); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("compaction-id", "2024-01-01T00:00:00.000Z"); + + Assert.assertEquals(200, response.getStatus()); + verifyAll(); + EasyMock.verify(compactionSpec, template, timelineView); + } + + @Test + public void testGetReindexingTimeline_invalidReferenceTimeFormat() + { + CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); + + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); + EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); + EasyMock.replay(compactionSpec); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("compaction-id", "not-a-date"); + + Assert.assertEquals(400, response.getStatus()); + Map entity = (Map) response.getEntity(); + Assert.assertTrue(entity.get("error").toString().contains("Invalid referenceTime format")); + verifyAll(); + EasyMock.verify(compactionSpec); + } + + @Test + public void testGetReindexingTimeline_serviceUnavailable() + { + EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.absent()); + replayAll(); + + Response response = supervisorResource.getReindexingTimeline("any-id", null); + + Assert.assertEquals(503, response.getStatus()); + verifyAll(); + } + private TestSeekableStreamSupervisorSpec createTestSpec(Integer taskCount, int taskCountMin) { HashMap autoScalerConfig = new HashMap<>(); From ccd6c71fc5d1e68abee4443585fd084d54a02adc Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 12:28:35 -0600 Subject: [PATCH 65/90] Refactor exception class for timeline to be more descriptive --- .../indexing/compact/CascadingReindexingTemplate.java | 10 +++++----- .../SegmentGranularityTimelineValidationException.java | 4 ++-- .../segment/transform/CompactionTransformSpec.java | 2 ++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 14b1b2a18953..2861887a37fc 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -327,7 +327,7 @@ public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) try { searchIntervals = generateAlignedSearchIntervals(referenceTime); } - catch (GranularityTimelineValidationException e) { + catch (SegmentGranularityTimelineValidationException e) { return createValidationErrorView( e, referenceTime, @@ -571,7 +571,7 @@ private InlineSchemaDataSourceCompactionConfig.Builder createBaseBuilder() * @param referenceTime the reference time for calculating period thresholds * @return list of split and aligned intervals with their granularities and source rules, ordered from oldest to newest * @throws IAE if no reindexing rules are configured - * @throws GranularityTimelineValidationException if granularities become coarser over time + * @throws SegmentGranularityTimelineValidationException if granularities become coarser over time */ List generateAlignedSearchIntervals(DateTime referenceTime) { @@ -696,7 +696,7 @@ private List splitIntervalAtPoints( * @param referenceTime the reference time for calculating period thresholds * @return base timeline with granularity-aligned intervals, ordered from oldest to newest * @throws IAE if no reindexing rules are configured - * @throws GranularityTimelineValidationException if granularities become coarser over time + * @throws SegmentGranularityTimelineValidationException if granularities become coarser over time */ private List generateBaseSegmentGranularityAlignedTimeline(DateTime referenceTime) { @@ -871,7 +871,7 @@ private List maybePrependRecentInterval( * which is typically undesirable and inefficient. * * @param timeline the completed base timeline with granularity information - * @throws GranularityTimelineValidationException if granularity becomes coarser as we move toward present + * @throws SegmentGranularityTimelineValidationException if granularity becomes coarser as we move toward present */ private void validateSegmentGranularityTimeline(List timeline) { @@ -891,7 +891,7 @@ private void validateSegmentGranularityTimeline(List ti // If the older interval's granularity is finer than the newer interval's granularity, // that means we're getting coarser as we move toward present, which is invalid. if (olderGran.isFinerThan(newerGran)) { - throw new GranularityTimelineValidationException( + throw new SegmentGranularityTimelineValidationException( dataSource, olderInterval.getInterval(), olderGran, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java index ffbf086ad4d2..7246af135c97 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java @@ -27,14 +27,14 @@ * Exception thrown when segment granularity timeline validation fails when using {@link CascadingReindexingTemplate}. * Contains structured information about the conflicting intervals. */ -public class GranularityTimelineValidationException extends IAE +public class SegmentGranularityTimelineValidationException extends IAE { private final Interval olderInterval; private final Granularity olderGranularity; private final Interval newerInterval; private final Granularity newerGranularity; - public GranularityTimelineValidationException( + public SegmentGranularityTimelineValidationException( String dataSource, Interval olderInterval, Granularity olderGranularity, diff --git a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java index 971a48e12049..1040315ed89f 100644 --- a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java +++ b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java @@ -20,6 +20,7 @@ package org.apache.druid.segment.transform; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.segment.VirtualColumns; @@ -71,6 +72,7 @@ public DimFilter getFilter() } @JsonProperty + @JsonInclude(JsonInclude.Include.NON_NULL) @Nullable public VirtualColumns getVirtualColumns() { From c7ed4e1e502370140979e62c75331b5376544efb Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 12:49:45 -0600 Subject: [PATCH 66/90] remove redundant naming patterns --- .../InlineReindexingRuleProvider.java | 130 +++++++++--------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index cd64db8aa1bd..cfcc6a2ccd39 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -88,27 +88,27 @@ public class InlineReindexingRuleProvider implements ReindexingRuleProvider { public static final String TYPE = "inline"; - private final List reindexingDeletionRules; - private final List reindexingIOConfigRules; - private final List reindexingSegmentGranularityRules; - private final List reindexingTuningConfigRules; - private final List reindexingDataSchemaRules; + private final List deletionRules; + private final List ioConfigRules; + private final List segmentGranularityRules; + private final List tuningConfigRules; + private final List dataSchemaRules; @JsonCreator public InlineReindexingRuleProvider( - @JsonProperty("reindexingDeletionRules") @Nullable List reindexingDeletionRules, - @JsonProperty("reindexingIOConfigRules") @Nullable List reindexingIOConfigRules, - @JsonProperty("reindexingSegmentGranularityRules") @Nullable List reindexingSegmentGranularityRules, - @JsonProperty("reindexingTuningConfigRules") @Nullable List reindexingTuningConfigRules, - @JsonProperty("reindexingDataSchemaRules") @Nullable List reindexingDataSchemaRules + @JsonProperty("deletionRules") @Nullable List deletionRules, + @JsonProperty("ioConfigRules") @Nullable List ioConfigRules, + @JsonProperty("segmentGranularityRules") @Nullable List segmentGranularityRules, + @JsonProperty("tuningConfigRules") @Nullable List tuningConfigRules, + @JsonProperty("dataSchemaRules") @Nullable List dataSchemaRules ) { - this.reindexingDeletionRules = Configs.valueOrDefault(reindexingDeletionRules, Collections.emptyList()); - this.reindexingIOConfigRules = Configs.valueOrDefault(reindexingIOConfigRules, Collections.emptyList()); - this.reindexingSegmentGranularityRules = Configs.valueOrDefault(reindexingSegmentGranularityRules, Collections.emptyList()); - this.reindexingTuningConfigRules = Configs.valueOrDefault(reindexingTuningConfigRules, Collections.emptyList()); - this.reindexingDataSchemaRules = Configs.valueOrDefault(reindexingDataSchemaRules, Collections.emptyList()); + this.deletionRules = Configs.valueOrDefault(deletionRules, Collections.emptyList()); + this.ioConfigRules = Configs.valueOrDefault(ioConfigRules, Collections.emptyList()); + this.segmentGranularityRules = Configs.valueOrDefault(segmentGranularityRules, Collections.emptyList()); + this.tuningConfigRules = Configs.valueOrDefault(tuningConfigRules, Collections.emptyList()); + this.dataSchemaRules = Configs.valueOrDefault(dataSchemaRules, Collections.emptyList()); } public static Builder builder() @@ -124,58 +124,58 @@ public String getType() } @Override - @JsonProperty("reindexingDeletionRules") + @JsonProperty public List getDeletionRules() { - return reindexingDeletionRules; + return deletionRules; } @Override @Nullable public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime referenceTime) { - return getApplicableRule(reindexingDataSchemaRules, interval, referenceTime); + return getApplicableRule(dataSchemaRules, interval, referenceTime); } @Override - @JsonProperty("reindexingDataSchemaRules") + @JsonProperty public List getDataSchemaRules() { - return reindexingDataSchemaRules; + return dataSchemaRules; } @Override - @JsonProperty("reindexingIOConfigRules") + @JsonProperty public List getIOConfigRules() { - return reindexingIOConfigRules; + return ioConfigRules; } @Override - @JsonProperty("reindexingSegmentGranularityRules") + @JsonProperty public List getSegmentGranularityRules() { - return reindexingSegmentGranularityRules; + return segmentGranularityRules; } @Override - @JsonProperty("reindexingTuningConfigRules") + @JsonProperty public List getTuningConfigRules() { - return reindexingTuningConfigRules; + return tuningConfigRules; } @Override public List getDeletionRules(Interval interval, DateTime referenceTime) { - return getApplicableRules(reindexingDeletionRules, interval, referenceTime); + return getApplicableRules(deletionRules, interval, referenceTime); } @Override @Nullable public ReindexingIOConfigRule getIOConfigRule(Interval interval, DateTime referenceTime) { - return getApplicableRule(reindexingIOConfigRules, interval, referenceTime); + return getApplicableRule(ioConfigRules, interval, referenceTime); } @Override @@ -185,14 +185,14 @@ public ReindexingSegmentGranularityRule getSegmentGranularityRule( DateTime referenceTime ) { - return getApplicableRule(reindexingSegmentGranularityRules, interval, referenceTime); + return getApplicableRule(segmentGranularityRules, interval, referenceTime); } @Override @Nullable public ReindexingTuningConfigRule getTuningConfigRule(Interval interval, DateTime referenceTime) { - return getApplicableRule(reindexingTuningConfigRules, interval, referenceTime); + return getApplicableRule(tuningConfigRules, interval, referenceTime); } /** @@ -251,22 +251,22 @@ public boolean equals(Object o) return false; } InlineReindexingRuleProvider that = (InlineReindexingRuleProvider) o; - return Objects.equals(reindexingDeletionRules, that.reindexingDeletionRules) - && Objects.equals(reindexingIOConfigRules, that.reindexingIOConfigRules) - && Objects.equals(reindexingSegmentGranularityRules, that.reindexingSegmentGranularityRules) - && Objects.equals(reindexingTuningConfigRules, that.reindexingTuningConfigRules) - && Objects.equals(reindexingDataSchemaRules, that.reindexingDataSchemaRules); + return Objects.equals(deletionRules, that.deletionRules) + && Objects.equals(ioConfigRules, that.ioConfigRules) + && Objects.equals(segmentGranularityRules, that.segmentGranularityRules) + && Objects.equals(tuningConfigRules, that.tuningConfigRules) + && Objects.equals(dataSchemaRules, that.dataSchemaRules); } @Override public int hashCode() { return Objects.hash( - reindexingDeletionRules, - reindexingIOConfigRules, - reindexingSegmentGranularityRules, - reindexingTuningConfigRules, - reindexingDataSchemaRules + deletionRules, + ioConfigRules, + segmentGranularityRules, + tuningConfigRules, + dataSchemaRules ); } @@ -274,60 +274,60 @@ public int hashCode() public String toString() { return "InlineReindexingRuleProvider{" - + "reindexingDeletionRules=" + reindexingDeletionRules - + ", reindexingIOConfigRules=" + reindexingIOConfigRules - + ", reindexingSegmentGranularityRules=" + reindexingSegmentGranularityRules - + ", reindexingTuningConfigRules=" + reindexingTuningConfigRules - + ", reindexingDataSchemaRules=" + reindexingDataSchemaRules + + "deletionRules=" + deletionRules + + ", ioConfigRules=" + ioConfigRules + + ", segmentGranularityRules=" + segmentGranularityRules + + ", tuningConfigRules=" + tuningConfigRules + + ", dataSchemaRules=" + dataSchemaRules + '}'; } public static class Builder { - private List reindexingDeletionRules; - private List reindexingIOConfigRules; - private List reindexingSegmentGranularityRules; - private List reindexingTuningConfigRules; - private List reindexingDataSchemaRules; + private List deletionRules; + private List ioConfigRules; + private List segmentGranularityRules; + private List tuningConfigRules; + private List dataSchemaRules; - public Builder deletionRules(List reindexingDeletionRules) + public Builder deletionRules(List deletionRules) { - this.reindexingDeletionRules = reindexingDeletionRules; + this.deletionRules = deletionRules; return this; } - public Builder dataSchemaRules(List reindexingDataSchemaRules) + public Builder dataSchemaRules(List dataSchemaRules) { - this.reindexingDataSchemaRules = reindexingDataSchemaRules; + this.dataSchemaRules = dataSchemaRules; return this; } - public Builder ioConfigRules(List reindexingIOConfigRules) + public Builder ioConfigRules(List ioConfigRules) { - this.reindexingIOConfigRules = reindexingIOConfigRules; + this.ioConfigRules = ioConfigRules; return this; } - public Builder segmentGranularityRules(List reindexingSegmentGranularityRules) + public Builder segmentGranularityRules(List segmentGranularityRules) { - this.reindexingSegmentGranularityRules = reindexingSegmentGranularityRules; + this.segmentGranularityRules = segmentGranularityRules; return this; } - public Builder tuningConfigRules(List reindexingTuningConfigRules) + public Builder tuningConfigRules(List tuningConfigRules) { - this.reindexingTuningConfigRules = reindexingTuningConfigRules; + this.tuningConfigRules = tuningConfigRules; return this; } public InlineReindexingRuleProvider build() { return new InlineReindexingRuleProvider( - reindexingDeletionRules, - reindexingIOConfigRules, - reindexingSegmentGranularityRules, - reindexingTuningConfigRules, - reindexingDataSchemaRules + deletionRules, + ioConfigRules, + segmentGranularityRules, + tuningConfigRules, + dataSchemaRules ); } } From f6ef9a99b7dea4661cffa455bc6530eefd7535ee Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 12:49:57 -0600 Subject: [PATCH 67/90] Add some java docs --- .../druid/server/compaction/ReindexingRule.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java index 358b4c27aba1..e2ed8ae42236 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingRule.java @@ -50,10 +50,24 @@ enum AppliesToMode NONE } + /** + * Provides an identifier for the rule, which can be used for referencing, logging, or management purposes. + *

    + * Note that there is no inherent contract for uniqueness across rule sets provided by this interface. + */ String getId(); + /** + * Provides a human-readable description of the rule, which can be used for logging, debugging, or user interfaces to explain the purpose and behavior of the rule. + */ String getDescription(); + /** + * Defines the age threshold for rule applicability. The rule applies to data older than a reference time minus this period. + *

    + * For example, if the period is "P30D" (30 days) and the reference time is "2024-01-31T00:00:00Z", the rule applies to data older than "2024-01-01T00:00:00Z". + * @return The period representing the age threshold for rule applicability. + */ Period getOlderThan(); /** From 83766fa50c2bdd5f3af229228d912a35abb02136 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 14:30:07 -0600 Subject: [PATCH 68/90] refactor how the final config customization works for cascading reindexing --- .../compact/CascadingReindexingTemplate.java | 70 +---- .../CompactionConfigBasedJobTemplate.java | 8 +- ...er.java => ReindexingConfigOptimizer.java} | 6 +- .../ReindexingDeletionRuleOptimizer.java | 63 +++- .../ReindexingDeletionRuleOptimizerTest.java | 281 ++++++++++-------- 5 files changed, 232 insertions(+), 196 deletions(-) rename indexing-service/src/main/java/org/apache/druid/indexing/compact/{ReindexingConfigFinalizer.java => ReindexingConfigOptimizer.java} (93%) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 2861887a37fc..eedb367ea0a5 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -30,12 +30,7 @@ import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.java.util.common.logger.Logger; import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.filter.DimFilter; -import org.apache.druid.query.filter.NotDimFilter; -import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; -import org.apache.druid.server.compaction.CompactionCandidate; -import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.compaction.IntervalGranularityInfo; import org.apache.druid.server.compaction.ReindexingConfigBuilder; import org.apache.druid.server.compaction.ReindexingRule; @@ -88,6 +83,8 @@ public class CascadingReindexingTemplate implements CompactionJobTemplate, DataSourceCompactionConfig { private static final Logger LOG = new Logger(CascadingReindexingTemplate.class); + private static final ReindexingConfigOptimizer DELETION_RULE_OPTIMIZER = new ReindexingDeletionRuleOptimizer(); + public static final String TYPE = "reindexCascade"; @@ -204,67 +201,6 @@ public Granularity getDefaultSegmentGranularity() return defaultSegmentGranularity; } - /** - * Creates a config finalizer that optimizes filter rules for cascading reindexing. - * When a candidate segment has already been reindexed with a subset of filter rules, - * this finalizer computes the minimal set of additional filter rules needed. - * This optimization reduces bitmap operations during reindexing. - */ - private static ReindexingConfigFinalizer createCascadingFinalizer() - { - return (config, candidate, params) -> { - // Only optimize if candidate has been reindexed before and config has a NotDimFilter - if (shouldOptimizeFilterRules(candidate, config)) { - - // Compute the minimal set of filter rules needed for this candidate - NotDimFilter reducedTransformSpecFilter = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - (NotDimFilter) config.getTransformSpec().getFilter(), - params.getFingerprintMapper() - ); - - // Filter virtual columns to only include ones referenced by the reduced filter - VirtualColumns reducedVirtualColumns = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( - reducedTransformSpecFilter, - config.getTransformSpec().getVirtualColumns() - ); - - // Safe cast: we know this is InlineSchemaDataSourceCompactionConfig because we just built it - return ((InlineSchemaDataSourceCompactionConfig) config) - .toBuilder() - .withTransformSpec(new CompactionTransformSpec(reducedTransformSpecFilter, reducedVirtualColumns)) - .build(); - } - - return config; // No optimization needed, return original config - }; - } - - /** - * Determines if we should optimize filter rules for this candidate. - * Returns true only if the candidate has been compacted before and has a NotDimFilter. - */ - private static boolean shouldOptimizeFilterRules( - CompactionCandidate candidate, - DataSourceCompactionConfig config - ) - { - if (candidate.getCurrentStatus() == null) { - return false; - } - - if (candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON)) { - return false; - } - - if (config.getTransformSpec() == null) { - return false; - } - - DimFilter filter = config.getTransformSpec().getFilter(); - return filter instanceof NotDimFilter; - } - /** * Creates a validation error view for timeline generation failures. * Logs the exception and returns a timeline view containing the validation error details. @@ -509,7 +445,7 @@ protected CompactionJobTemplate createJobTemplateForInterval( InlineSchemaDataSourceCompactionConfig config ) { - return new CompactionConfigBasedJobTemplate(config, createCascadingFinalizer()); + return new CompactionConfigBasedJobTemplate(config, DELETION_RULE_OPTIMIZER); } /** diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java index e29f795b4b34..da0a4dd11d64 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java @@ -48,16 +48,16 @@ public class CompactionConfigBasedJobTemplate implements CompactionJobTemplate { private final DataSourceCompactionConfig config; - private final ReindexingConfigFinalizer configFinalizer; + private final ReindexingConfigOptimizer configFinalizer; public CompactionConfigBasedJobTemplate(DataSourceCompactionConfig config) { - this(config, ReindexingConfigFinalizer.IDENTITY); + this(config, ReindexingConfigOptimizer.IDENTITY); } public CompactionConfigBasedJobTemplate( DataSourceCompactionConfig config, - ReindexingConfigFinalizer configFinalizer + ReindexingConfigOptimizer configFinalizer ) { this.config = config; @@ -93,7 +93,7 @@ public List createCompactionJobs( final CompactionCandidate candidate = segmentIterator.next(); // Allow template-specific customization of the config per candidate - DataSourceCompactionConfig finalConfig = configFinalizer.finalizeConfig(config, candidate, params); + DataSourceCompactionConfig finalConfig = configFinalizer.optimizeConfig(config, candidate, params); ClientCompactionTaskQuery taskPayload = CompactSegments.createCompactionTask( candidate, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigOptimizer.java similarity index 93% rename from indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigOptimizer.java index a432705fe3ca..d091d5433f22 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigFinalizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigOptimizer.java @@ -31,7 +31,7 @@ * the candidate's indexing state, while simpler templates can use the identity finalizer. */ @FunctionalInterface -public interface ReindexingConfigFinalizer +public interface ReindexingConfigOptimizer { /** * Customize the reindexing config for a specific candidate. @@ -41,7 +41,7 @@ public interface ReindexingConfigFinalizer * @param params the reindexing job parameters * @return the finalized config to use for this candidate (this may be the same as input or a modified version) */ - DataSourceCompactionConfig finalizeConfig( + DataSourceCompactionConfig optimizeConfig( DataSourceCompactionConfig config, CompactionCandidate candidate, CompactionJobParams params @@ -51,5 +51,5 @@ DataSourceCompactionConfig finalizeConfig( * Identity finalizer that returns the config unchanged. * Use this for templates that don't need per-candidate customization. */ - ReindexingConfigFinalizer IDENTITY = (config, candidate, params) -> config; + ReindexingConfigOptimizer IDENTITY = (config, candidate, params) -> config; } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java index 823db26b5577..2dbd7101b0f7 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java @@ -26,8 +26,12 @@ import org.apache.druid.segment.VirtualColumn; import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; @@ -48,10 +52,38 @@ * be wasteful and redundant. This class provides funcionality to optimize the set of rules to be applied by * any given reindexing task. */ -public class ReindexingDeletionRuleOptimizer +public class ReindexingDeletionRuleOptimizer implements ReindexingConfigOptimizer { private static final Logger LOG = new Logger(ReindexingDeletionRuleOptimizer.class); + @Override + public DataSourceCompactionConfig optimizeConfig( + DataSourceCompactionConfig config, + CompactionCandidate candidate, + CompactionJobParams params + ) + { + if (!shouldOptimizeFilterRules(candidate, config)) { + return config; + } + + NotDimFilter reducedFilter = computeRequiredSetOfFilterRulesForCandidate( + candidate, + (NotDimFilter) config.getTransformSpec().getFilter(), + params.getFingerprintMapper() // Available from params! + ); + + VirtualColumns reducedVirtualColumns = filterVirtualColumnsForFilter( + reducedFilter, + config.getTransformSpec().getVirtualColumns() + ); + + return ((InlineSchemaDataSourceCompactionConfig) config) + .toBuilder() + .withTransformSpec(new CompactionTransformSpec(reducedFilter, reducedVirtualColumns)) + .build(); + } + /** * Computes the required set of deletion rules to be applied for the given {@link CompactionCandidate}. *

    @@ -65,7 +97,7 @@ public class ReindexingDeletionRuleOptimizer * @return the set of unapplied deletion rules wrapped in a NotDimFilter, or null if all rules have been applied */ @Nullable - public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( + private NotDimFilter computeRequiredSetOfFilterRulesForCandidate( CompactionCandidate candidateSegments, NotDimFilter expectedFilter, IndexingStateFingerprintMapper fingerprintMapper @@ -133,7 +165,7 @@ public static NotDimFilter computeRequiredSetOfFilterRulesForCandidate( * @return filtered VirtualColumns with only referenced columns, or null if none are referenced */ @Nullable - public static VirtualColumns filterVirtualColumnsForFilter( + private VirtualColumns filterVirtualColumnsForFilter( @Nullable DimFilter filter, @Nullable VirtualColumns virtualColumns ) @@ -187,4 +219,29 @@ private static Set extractAppliedFilters(CompactionState state) return Collections.singleton(inner); } } + + /** + * Determines if we should optimize filter rules for this candidate. + * Returns true only if the candidate has been compacted before and has a NotDimFilter. + */ + private boolean shouldOptimizeFilterRules( + CompactionCandidate candidate, + DataSourceCompactionConfig config + ) + { + if (candidate.getCurrentStatus() == null) { + return false; + } + + if (candidate.getCurrentStatus().getReason().equals(CompactionStatus.NEVER_COMPACTED_REASON)) { + return false; + } + + if (config.getTransformSpec() == null) { + return false; + } + + DimFilter filter = config.getTransformSpec().getFilter(); + return filter instanceof NotDimFilter; + } } diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java index cf8a967dc4a2..4e1c92b36f43 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java @@ -39,14 +39,18 @@ import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.server.coordinator.DataSourceCompactionConfig; +import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.timeline.CompactionState; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentId; +import org.easymock.EasyMock; import org.joda.time.DateTime; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -64,6 +68,7 @@ public class ReindexingDeletionRuleOptimizerTest private HeapMemoryIndexingStateStorage indexingStateStorage; private IndexingStateCache indexingStateCache; private IndexingStateFingerprintMapper fingerprintMapper; + private ReindexingDeletionRuleOptimizer optimizer; @Before public void setUp() @@ -74,46 +79,46 @@ public void setUp() indexingStateCache, new DefaultObjectMapper() ); + optimizer = new ReindexingDeletionRuleOptimizer(); } @Test - public void testComputeRequiredFilters_SingleFilter_NotOrFilter_ReturnsAsIs() + public void testOptimize_SingleFilter_NotOrFilter_NoFingerprints_ReturnsUnchanged() { DimFilter filterA = new SelectorDimFilter("country", "US", null); NotDimFilter expectedFilter = new NotDimFilter(filterA); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertEquals(expectedFilter, result); + // No state for fp1, so config should be unchanged + Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } @Test - public void test_computeRequiredSetOfFilterRulesForCandidate_oneFilter_nothingToApply() + public void testOptimize_SingleFilter_AlreadyApplied_RemovesTransformSpec() { DimFilter filterB = new SelectorDimFilter("country", "UK", null); - CompactionState state = createStateWithFilters(filterB); + CompactionState state = createStateWithSingleFilter(filterB); indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, "fp1", state, DateTimes.nowUtc()); syncCacheFromManager(); + NotDimFilter expectedFilter = new NotDimFilter(filterB); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertNull(result); + // All filters optimized away, transform spec should be null + Assert.assertNull(result.getTransformSpec()); } @Test - public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() + public void testOptimize_AllFiltersAlreadyApplied_RemovesTransformSpec() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -125,18 +130,17 @@ public void testComputeRequiredFilters_AllFiltersAlreadyApplied_ReturnsNull() NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertNull(result); + // All filters already applied, transform spec should be removed + Assert.assertNull(result.getTransformSpec()); } @Test - public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() + public void testOptimize_NoFiltersApplied_ReturnsAllExpected() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -148,20 +152,20 @@ public void testComputeRequiredFilters_NoFiltersApplied_ReturnsAllExpected() NotDimFilter expectedFilter = new NotDimFilter(new OrDimFilter(Arrays.asList(filterA, filterB, filterC))); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - OrDimFilter innerOr = (OrDimFilter) result.getField(); + // No filters were applied, so all should remain + NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); + OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Assert.assertEquals(3, innerOr.getFields().size()); Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); } @Test - public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() + public void testOptimize_PartiallyApplied_ReturnsDelta() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -176,14 +180,14 @@ public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - OrDimFilter innerOr = (OrDimFilter) result.getField(); + // Only C and D should remain (A and B were already applied) + NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); + OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Set resultSet = new HashSet<>(innerOr.getFields()); Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); @@ -191,7 +195,7 @@ public void testComputeRequiredFilters_PartiallyApplied_ReturnsDelta() } @Test - public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() + public void testOptimize_MultipleFingerprints_UnionOfMissing() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -209,14 +213,15 @@ public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() new OrDimFilter(Arrays.asList(filterA, filterB, filterC, filterD)) ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - OrDimFilter innerOr = (OrDimFilter) result.getField(); + // fp1 has A,B applied; fp2 has A,C applied + // Union of missing: B (missing from fp2), C (missing from fp1), D (missing from both) + NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); + OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Set resultSet = new HashSet<>(innerOr.getFields()); Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); @@ -224,7 +229,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_UnionOfMissing() } @Test - public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() + public void testOptimize_MultipleFingerprints_NoDuplicates() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -241,14 +246,14 @@ public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1", "fp2"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - OrDimFilter innerOr = (OrDimFilter) result.getField(); + // Both fingerprints have A applied, so only B and C remain + NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); + OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Set resultSet = new HashSet<>(innerOr.getFields()); Assert.assertEquals(2, resultSet.size()); @@ -256,7 +261,7 @@ public void testComputeRequiredFilters_MultipleFingerprints_NoDuplicates() } @Test - public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters() + public void testOptimize_MissingCompactionState_ReturnsAllFilters() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -267,18 +272,17 @@ public void testComputeRequiredFilters_MissingCompactionState_ReturnsAllFilters( new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertEquals(expectedFilter, result); + // No state available, all filters should remain + Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } @Test - public void testComputeRequiredFilters_TransformSpecWithSingleFilter() + public void testOptimize_TransformSpecWithSingleFilter() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -292,20 +296,20 @@ public void testComputeRequiredFilters_TransformSpecWithSingleFilter() new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) ); CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - OrDimFilter innerOr = (OrDimFilter) result.getField(); + // A was already applied, only B and C should remain + NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); + OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Assert.assertEquals(2, innerOr.getFields().size()); Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); } @Test - public void testComputeRequiredFilters_SegmentsWithNoFingerprints() + public void testOptimize_SegmentsWithNoFingerprints() { DimFilter filterA = new SelectorDimFilter("country", "US", null); DimFilter filterB = new SelectorDimFilter("country", "UK", null); @@ -316,20 +320,42 @@ public void testComputeRequiredFilters_SegmentsWithNoFingerprints() NotDimFilter expectedFilter = new NotDimFilter( new OrDimFilter(Arrays.asList(filterA, filterB, filterC)) ); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); - NotDimFilter result = ReindexingDeletionRuleOptimizer.computeRequiredSetOfFilterRulesForCandidate( - candidate, - expectedFilter, - fingerprintMapper - ); + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertEquals(expectedFilter, result); + // No fingerprints, all filters should remain + Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } - // Helper methods for filter tests + // Helper methods + + private InlineSchemaDataSourceCompactionConfig createConfigWithFilter( + @Nullable NotDimFilter filter, + @Nullable VirtualColumns virtualColumns + ) + { + CompactionTransformSpec transformSpec = filter == null && virtualColumns == null + ? null + : new CompactionTransformSpec(filter, virtualColumns); + + return InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource(TestDataSource.WIKI) + .withTransformSpec(transformSpec) + .build(); + } + + private CompactionJobParams createParams() + { + CompactionJobParams mockParams = EasyMock.createMock(CompactionJobParams.class); + EasyMock.expect(mockParams.getFingerprintMapper()).andReturn(fingerprintMapper).anyTimes(); + EasyMock.replay(mockParams); + return mockParams; + } private CompactionCandidate createCandidateWithFingerprints(String... fingerprints) { @@ -395,50 +421,40 @@ private CompactionState createStateWithoutFilters() } @Test - public void testFilterVirtualColumnsForFilter_someColumnsReferenced() + public void testOptimize_FilterVirtualColumns_SomeColumnsReferenced() { - // Create virtual columns + // Create virtual columns vc1, vc2, vc3 VirtualColumns virtualColumns = VirtualColumns.create( ImmutableList.of( - new ExpressionVirtualColumn( - "vc1", - "col1 + 1", - ColumnType.LONG, - TestExprMacroTable.INSTANCE - ), - new ExpressionVirtualColumn( - "vc2", - "col2 + 2", - ColumnType.LONG, - TestExprMacroTable.INSTANCE - ), - new ExpressionVirtualColumn( - "vc3", - "col3 + 3", - ColumnType.LONG, - TestExprMacroTable.INSTANCE - ) + new ExpressionVirtualColumn("vc1", "col1 + 1", ColumnType.LONG, TestExprMacroTable.INSTANCE), + new ExpressionVirtualColumn("vc2", "col2 + 2", ColumnType.LONG, TestExprMacroTable.INSTANCE), + new ExpressionVirtualColumn("vc3", "col3 + 3", ColumnType.LONG, TestExprMacroTable.INSTANCE) ) ); - // Create a filter that only references vc1 and vc3 + // Create a filter that only references vc1 and vc3 (vc2 is unreferenced) DimFilter filter = new OrDimFilter( Arrays.asList( new SelectorDimFilter("vc1", "value1", null), new SelectorDimFilter("vc3", "value3", null) ) ); + NotDimFilter notFilter = new NotDimFilter(filter); - VirtualColumns filtered = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( - filter, - virtualColumns - ); + // Candidate has no filters applied, so all filters remain + CompactionCandidate candidate = createCandidateWithNullFingerprints(1); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(notFilter, virtualColumns); + CompactionJobParams params = createParams(); + + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertNotNull(filtered); - Assert.assertEquals(2, filtered.getVirtualColumns().length); + // Filter remains, but vc2 should be filtered out + VirtualColumns resultVCs = result.getTransformSpec().getVirtualColumns(); + Assert.assertNotNull(resultVCs); + Assert.assertEquals(2, resultVCs.getVirtualColumns().length); Set outputNames = new HashSet<>(); - for (org.apache.druid.segment.VirtualColumn vc : filtered.getVirtualColumns()) { + for (org.apache.druid.segment.VirtualColumn vc : resultVCs.getVirtualColumns()) { outputNames.add(vc.getOutputName()); } @@ -448,36 +464,63 @@ public void testFilterVirtualColumnsForFilter_someColumnsReferenced() } @Test - public void testFilterVirtualColumnsForFilter_noColumnsReferenced() + public void testOptimize_FilterVirtualColumns_NoColumnsReferenced() { // Create virtual columns VirtualColumns virtualColumns = VirtualColumns.create( ImmutableList.of( - new ExpressionVirtualColumn( - "vc1", - "col1 + 1", - ColumnType.LONG, - TestExprMacroTable.INSTANCE - ), - new ExpressionVirtualColumn( - "vc2", - "col2 + 2", - ColumnType.LONG, - TestExprMacroTable.INSTANCE - ) + new ExpressionVirtualColumn("vc1", "col1 + 1", ColumnType.LONG, TestExprMacroTable.INSTANCE), + new ExpressionVirtualColumn("vc2", "col2 + 2", ColumnType.LONG, TestExprMacroTable.INSTANCE) ) ); // Create a filter that references a physical column, not virtual columns DimFilter filter = new SelectorDimFilter("regularColumn", "value", null); + NotDimFilter notFilter = new NotDimFilter(filter); - VirtualColumns filtered = ReindexingDeletionRuleOptimizer.filterVirtualColumnsForFilter( - filter, - virtualColumns - ); + // Candidate has no filters applied + CompactionCandidate candidate = createCandidateWithNullFingerprints(1); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(notFilter, virtualColumns); + CompactionJobParams params = createParams(); + + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); + + // Virtual columns should be null when no virtual columns are referenced + Assert.assertNull(result.getTransformSpec().getVirtualColumns()); + } + + @Test + public void testOptimize_CandidateNeverCompacted_NoOptimization() + { + DimFilter filterA = new SelectorDimFilter("country", "US", null); + NotDimFilter expectedFilter = new NotDimFilter(filterA); + + // Candidate with NEVER_COMPACTED status + CompactionCandidate candidate = createCandidateWithNullFingerprints(1); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); + CompactionJobParams params = createParams(); + + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); + + // Should return config unchanged since candidate was never compacted + Assert.assertSame(config, result); + } + + @Test + public void testOptimize_NoTransformSpec_NoOptimization() + { + // Config without transform spec + InlineSchemaDataSourceCompactionConfig config = InlineSchemaDataSourceCompactionConfig.builder() + .forDataSource(TestDataSource.WIKI) + .build(); + + CompactionCandidate candidate = createCandidateWithFingerprints("fp1"); + CompactionJobParams params = createParams(); + + DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - // Should return null when no virtual columns are referenced so all are filtered out - Assert.assertNull(filtered); + // Should return config unchanged + Assert.assertSame(config, result); } /** From f07b97a8b642af3ead7fab15d19810752c0872e9 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 16:19:05 -0600 Subject: [PATCH 69/90] fix serde issues I was having --- .../compaction/InlineReindexingRuleProvider.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java index cfcc6a2ccd39..d713b1ec3a9f 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java +++ b/server/src/main/java/org/apache/druid/server/compaction/InlineReindexingRuleProvider.java @@ -124,7 +124,7 @@ public String getType() } @Override - @JsonProperty + @JsonProperty("deletionRules") public List getDeletionRules() { return deletionRules; @@ -138,28 +138,28 @@ public ReindexingDataSchemaRule getDataSchemaRule(Interval interval, DateTime re } @Override - @JsonProperty + @JsonProperty("dataSchemaRules") public List getDataSchemaRules() { return dataSchemaRules; } @Override - @JsonProperty + @JsonProperty("ioConfigRules") public List getIOConfigRules() { return ioConfigRules; } @Override - @JsonProperty + @JsonProperty("segmentGranularityRules") public List getSegmentGranularityRules() { return segmentGranularityRules; } @Override - @JsonProperty + @JsonProperty("tuningConfigRules") public List getTuningConfigRules() { return tuningConfigRules; From 707904f5f12440dd50f89693f6c9a7f4ca86e825 Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 16:22:18 -0600 Subject: [PATCH 70/90] fixup random bits after CI ran --- .../embedded/compact/CompactionSupervisorTest.java | 2 +- .../compact/ReindexingDeletionRuleOptimizer.java | 8 ++++++-- .../ReindexingDeletionRuleOptimizerTest.java | 13 ++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index fe7c09ecf54f..192b83200f8e 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -335,7 +335,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac null, null, null, - Granularities.DAY + Granularities.HOUR ); runCompactionWithSpec(cascadingReindexingTemplate); waitForAllCompactionTasksToFinish(); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java index 2dbd7101b0f7..9b4bbb597977 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizer.java @@ -70,7 +70,7 @@ public DataSourceCompactionConfig optimizeConfig( NotDimFilter reducedFilter = computeRequiredSetOfFilterRulesForCandidate( candidate, (NotDimFilter) config.getTransformSpec().getFilter(), - params.getFingerprintMapper() // Available from params! + params.getFingerprintMapper() ); VirtualColumns reducedVirtualColumns = filterVirtualColumnsForFilter( @@ -78,9 +78,13 @@ public DataSourceCompactionConfig optimizeConfig( config.getTransformSpec().getVirtualColumns() ); + CompactionTransformSpec transformSpec = (reducedFilter == null && reducedVirtualColumns == null) + ? null + : new CompactionTransformSpec(reducedFilter, reducedVirtualColumns); + return ((InlineSchemaDataSourceCompactionConfig) config) .toBuilder() - .withTransformSpec(new CompactionTransformSpec(reducedFilter, reducedVirtualColumns)) + .withTransformSpec(transformSpec) .build(); } diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java index 4e1c92b36f43..5f4eafb0f878 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java @@ -39,6 +39,7 @@ import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.compaction.CompactionCandidate; +import org.apache.druid.server.compaction.CompactionStatus; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.timeline.CompactionState; @@ -362,7 +363,8 @@ private CompactionCandidate createCandidateWithFingerprints(String... fingerprin List segments = Arrays.stream(fingerprints) .map(fp -> DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(fp).build()) .collect(Collectors.toList()); - return CompactionCandidate.from(segments, null); + return CompactionCandidate.from(segments, null) + .withCurrentStatus(CompactionStatus.pending("segments need compaction")); } private CompactionCandidate createCandidateWithNullFingerprints(int count) @@ -371,7 +373,8 @@ private CompactionCandidate createCandidateWithNullFingerprints(int count) for (int i = 0; i < count; i++) { segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); } - return CompactionCandidate.from(segments, null); + return CompactionCandidate.from(segments, null) + .withCurrentStatus(CompactionStatus.pending("segments need compaction")); } private CompactionState createStateWithFilters(DimFilter... filters) @@ -496,7 +499,11 @@ public void testOptimize_CandidateNeverCompacted_NoOptimization() NotDimFilter expectedFilter = new NotDimFilter(filterA); // Candidate with NEVER_COMPACTED status - CompactionCandidate candidate = createCandidateWithNullFingerprints(1); + List segments = new ArrayList<>(); + segments.add(DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(null).build()); + CompactionCandidate candidate = CompactionCandidate.from(segments, null) + .withCurrentStatus(CompactionStatus.pending(CompactionStatus.NEVER_COMPACTED_REASON)); + InlineSchemaDataSourceCompactionConfig config = createConfigWithFilter(expectedFilter, null); CompactionJobParams params = createParams(); From a24cd322d16a28e55a311978259ebb4b611a775d Mon Sep 17 00:00:00 2001 From: capistrant Date: Mon, 16 Feb 2026 18:01:40 -0600 Subject: [PATCH 71/90] waiting for segments to be available should avoid issues with segs that are deleted showing up --- .../testing/embedded/compact/CompactionSupervisorTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index 192b83200f8e..c3c160492e08 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -339,6 +339,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac ); runCompactionWithSpec(cascadingReindexingTemplate); waitForAllCompactionTasksToFinish(); + cluster.callApi().waitForAllSegmentsToBeAvailable(dataSource, coordinator, broker); Assertions.assertEquals(4, getNumSegmentsWith(Granularities.FIFTEEN_MINUTE)); Assertions.assertEquals(5, getNumSegmentsWith(Granularities.HOUR)); @@ -536,6 +537,7 @@ public void test_compactionWithTransformFilteringAllRows_createsTombstones( runCompactionWithSpec(compactionConfig); waitForAllCompactionTasksToFinish(); + cluster.callApi().waitForAllSegmentsToBeAvailable(dataSource, coordinator, broker); int finalSegmentCount = getNumSegmentsWith(Granularities.DAY); Assertions.assertEquals( From bfff8de7f2646b59fe5d2ab412f53290113bf516 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 17 Feb 2026 10:35:51 -0600 Subject: [PATCH 72/90] Move ReindexingConfigBuilder to a hopefully better place --- .../compact/CascadingReindexingTemplate.java | 1 - .../compact}/ReindexingConfigBuilder.java | 32 ++++++++++++------- .../compact}/ReindexingConfigBuilderTest.java | 10 +++++- 3 files changed, 30 insertions(+), 13 deletions(-) rename {server/src/main/java/org/apache/druid/server/compaction => indexing-service/src/main/java/org/apache/druid/indexing/compact}/ReindexingConfigBuilder.java (87%) rename {server/src/test/java/org/apache/druid/server/compaction => indexing-service/src/test/java/org/apache/druid/indexing/compact}/ReindexingConfigBuilderTest.java (95%) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index eedb367ea0a5..eb589b036dbb 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -32,7 +32,6 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.compaction.IntervalGranularityInfo; -import org.apache.druid.server.compaction.ReindexingConfigBuilder; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigBuilder.java similarity index 87% rename from server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java rename to indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigBuilder.java index 5a88ae1369f1..8fec45532105 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingConfigBuilder.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingConfigBuilder.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.druid.server.compaction; +package org.apache.druid.indexing.compact; import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.logger.Logger; @@ -27,6 +27,13 @@ import org.apache.druid.segment.VirtualColumn; import org.apache.druid.segment.VirtualColumns; import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.server.compaction.IntervalGranularityInfo; +import org.apache.druid.server.compaction.ReindexingDataSchemaRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; +import org.apache.druid.server.compaction.ReindexingRule; +import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.joda.time.DateTime; import org.joda.time.Interval; @@ -37,10 +44,13 @@ import java.util.List; /** - * Builds compaction configs by applying reindexing rules. - * Encapsulates the logic for combining additive rules and applying all rule types. + * Builds compaction configs for cascading reindexing by applying reindexing rules. + * This is an implementation detail of {@link CascadingReindexingTemplate} and encapsulates + * the logic for combining additive rules and applying all rule types. + *

    + * Package-private as this is only used internally by CascadingReindexingTemplate. */ -public class ReindexingConfigBuilder +class ReindexingConfigBuilder { private static final Logger LOG = new Logger(ReindexingConfigBuilder.class); @@ -53,12 +63,12 @@ public class ReindexingConfigBuilder * Result of applying reindexing rules to a config builder. * Contains both the count of rules applied and the actual rules that were applied. */ - public static class BuildResult + static class BuildResult { private final int ruleCount; private final List appliedRules; - public BuildResult(int ruleCount, List appliedRules) + BuildResult(int ruleCount, List appliedRules) { this.ruleCount = ruleCount; this.appliedRules = appliedRules; @@ -71,7 +81,7 @@ public BuildResult(int ruleCount, List appliedRules) * * @return the number of rules that were applied to the builder */ - public int getRuleCount() + int getRuleCount() { return ruleCount; } @@ -79,13 +89,13 @@ public int getRuleCount() /** * @return immutable list of the actual rules that were applied, in application order */ - public List getAppliedRules() + List getAppliedRules() { return appliedRules; } } - public ReindexingConfigBuilder( + ReindexingConfigBuilder( ReindexingRuleProvider provider, Interval interval, DateTime referenceTime, @@ -103,7 +113,7 @@ public ReindexingConfigBuilder( * * @return number of rules applied */ - public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) + int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) { return applyToWithDetails(builder).getRuleCount(); } @@ -114,7 +124,7 @@ public int applyTo(InlineSchemaDataSourceCompactionConfig.Builder builder) * * @return BuildResult containing the count and list of applied rules */ - public BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Builder builder) + BuildResult applyToWithDetails(InlineSchemaDataSourceCompactionConfig.Builder builder) { int count = 0; List appliedRules = new ArrayList<>(); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java similarity index 95% rename from server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java rename to indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java index 62a7ca8e9f5f..396200e803ec 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingConfigBuilderTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java @@ -17,7 +17,7 @@ * under the License. */ -package org.apache.druid.server.compaction; +package org.apache.druid.indexing.compact; import com.google.common.collect.ImmutableList; import org.apache.druid.data.input.impl.AggregateProjectionSpec; @@ -30,6 +30,14 @@ import org.apache.druid.query.filter.NotDimFilter; import org.apache.druid.query.filter.OrDimFilter; import org.apache.druid.query.filter.SelectorDimFilter; +import org.apache.druid.server.compaction.InlineReindexingRuleProvider; +import org.apache.druid.server.compaction.IntervalGranularityInfo; +import org.apache.druid.server.compaction.ReindexingDataSchemaRule; +import org.apache.druid.server.compaction.ReindexingDeletionRule; +import org.apache.druid.server.compaction.ReindexingIOConfigRule; +import org.apache.druid.server.compaction.ReindexingRuleProvider; +import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; +import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; From 3f2747c8961e31d7daf3979ef5379b7d4e89423f Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 17 Feb 2026 14:30:44 -0600 Subject: [PATCH 73/90] Remove web-console changes from this PR --- web-console/src/components/index.ts | 1 - .../reindexing-timeline.spec.tsx.snap | 694 ------------------ .../reindexing-timeline.scss | 202 ----- .../reindexing-timeline.spec.tsx | 460 ------------ .../reindexing-timeline.tsx | 690 ----------------- ...pervisor-table-action-dialog.spec.tsx.snap | 27 - .../supervisor-table-action-dialog.tsx | 13 +- 7 files changed, 3 insertions(+), 2084 deletions(-) delete mode 100644 web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap delete mode 100644 web-console/src/components/reindexing-timeline/reindexing-timeline.scss delete mode 100644 web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx delete mode 100644 web-console/src/components/reindexing-timeline/reindexing-timeline.tsx diff --git a/web-console/src/components/index.ts b/web-console/src/components/index.ts index bf28a4c501b7..849cc470ccf5 100644 --- a/web-console/src/components/index.ts +++ b/web-console/src/components/index.ts @@ -48,7 +48,6 @@ export * from './portal-bubble/portal-bubble'; export * from './query-error-pane/query-error-pane'; export * from './record-table-pane/record-table-pane'; export * from './refresh-button/refresh-button'; -export * from './reindexing-timeline/reindexing-timeline'; export * from './rule-editor/rule-editor'; export * from './segment-timeline/segment-timeline'; export * from './show-json/show-json'; diff --git a/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap b/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap deleted file mode 100644 index 25db7747dc8e..000000000000 --- a/web-console/src/components/reindexing-timeline/__snapshots__/reindexing-timeline.spec.tsx.snap +++ /dev/null @@ -1,694 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ReindexingTimeline component rendering matches snapshot for empty intervals 1`] = ` -

    -
    - - No reindexing intervals found. This may indicate that the rule provider is not ready or no rules are configured. -
    -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot for error state 1`] = ` -
    -
    - -
    - Error loading reindexing timeline -
    - Failed to load timeline -
    -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot for loading state 1`] = ` -
    - -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot with normal intervals 1`] = ` -
    -
    -
    - - DataSource: - - - test-datasource - - | - - - - Reference Time: - - - - Nov 15, 2024 12:00 PM UTC -
    -
    -
    - -
    - Click on an interval to view its configuration details -
    -
    -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot with skip offset applied 1`] = ` -
    -
    -
    - - DataSource: - - - test-datasource - - | - - - - Reference Time: - - - - Nov 15, 2024 12:00 PM UTC -
    -
    - - - - Skip Offset: - skipOffsetFromNow - ( - P7D - ) - - -
    -
    -
    - -
    - Click on an interval to view its configuration details -
    -
    -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot with skip offset not applied 1`] = ` -
    -
    -
    - - DataSource: - - - test-datasource - - | - - - - Reference Time: - - - - Nov 15, 2024 12:00 PM UTC -
    -
    - - - - - skipOffsetFromLatest - ( - P7D - ): Not reflected in this preview - - - - -
    -
    -
    - -
    - Click on an interval to view its configuration details -
    -
    -
    -`; - -exports[`ReindexingTimeline component rendering matches snapshot with validation error 1`] = ` -
    -
    - -
    - Invalid Supervisor Configuration -
    -

    - - The segment granularity rule definitions have created an illegal segment granularity timeline. - -

    -

    - Segment granularity must stay the same or become - - coarser - - as data ages from present to past. Your configuration violates this constraint: -

    -
      -
    • - - Older interval - - (further in the past): - - - 2024-01-01/2024-02-01 - - has - - - finer - - granularity: - - - - HOUR - - -
    • -
    • - - Newer interval - - (more recent): - - - 2024-02-01/2024-03-01 - - has - - - coarser - - granularity: - - - - DAY - - -
    • -
    -

    - - To fix this: - - Adjust your segment granularity rules so that as time moves from present to past, the granularity either stays the same or gets coarser (e.g., HOUR → DAY → MONTH → YEAR). -

    -
    -
    -`; diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.scss b/web-console/src/components/reindexing-timeline/reindexing-timeline.scss deleted file mode 100644 index 92882ceb0f87..000000000000 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.scss +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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. - */ - -.reindexing-timeline { - padding: 20px; - max-width: 1200px; - - .bp5-callout { - ul { - margin: 10px 0; - padding-left: 20px; - - li { - margin: 8px 0; - line-height: 1.6; - - code { - background-color: rgba(0, 0, 0, 0.1); - padding: 2px 6px; - border-radius: 3px; - font-family: monospace; - font-size: 13px; - } - - .bp5-tag { - margin-left: 5px; - vertical-align: middle; - } - } - } - - p { - margin: 8px 0; - line-height: 1.6; - - &:first-child { - margin-top: 0; - } - - &:last-child { - margin-bottom: 0; - } - } - } - - .timeline-header { - margin-bottom: 20px; - - .header-info { - margin-bottom: 10px; - font-size: 14px; - - .spacer { - margin: 0 10px; - color: #5c7080; - } - - strong { - margin-right: 5px; - } - } - - .skip-offset-info { - display: flex; - gap: 10px; - } - } - - .timeline-bar-container { - margin-bottom: 20px; - padding: 20px; - - .timeline-bar { - display: flex; - gap: 2px; - min-height: 120px; - border-radius: 4px; - overflow: hidden; - - .timeline-segment { - cursor: pointer; - transition: all 0.2s ease; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 10px 5px; - min-width: 60px; - border-radius: 3px; - position: relative; - - &:hover { - opacity: 1 !important; - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - } - - &.selected { - box-shadow: 0 0 0 3px rgba(19, 124, 189, 0.6); - transform: translateY(-2px); - } - - .segment-label { - color: white; - text-align: center; - font-size: 11px; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); - - .segment-date { - margin-bottom: 2px; - font-weight: 500; - } - - .segment-rules { - margin-top: 8px; - - .bp5-tag { - background-color: rgba(255, 255, 255, 0.9); - color: #333; - } - } - } - } - } - - .timeline-hint { - margin-top: 10px; - text-align: center; - font-size: 12px; - color: #5c7080; - font-style: italic; - } - } - - .interval-detail-panel { - margin-top: 20px; - padding: 20px; - - .detail-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 20px; - padding-bottom: 15px; - border-bottom: 1px solid #d3d8de; - - h3 { - margin: 0 0 10px 0; - font-size: 16px; - } - } - - .detail-content { - .config-badges { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-bottom: 20px; - } - - .full-config-section { - margin-top: 20px; - padding-top: 20px; - border-top: 1px solid #d3d8de; - display: flex; - gap: 10px; - justify-content: center; - } - } - } -} - -.reindexing-config-dialog { - width: 80%; - max-width: 900px; - - .bp5-dialog-body { - margin: 0; - padding: 0; - } - - .config-json-viewer { - .ace_editor { - font-size: 12px; - } - } -} diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx deleted file mode 100644 index 315b7b1dc7c0..000000000000 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.spec.tsx +++ /dev/null @@ -1,460 +0,0 @@ -/* - * 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. - */ - -import { fireEvent, render } from '@testing-library/react'; - -import type { useQueryManager as UseQueryManager } from '../../hooks'; -import * as hooks from '../../hooks'; -import * as singletons from '../../singletons'; -import { QueryState } from '../../utils'; - -import { ReindexingTimeline } from './reindexing-timeline'; - -// Mock the useQueryManager hook -jest.mock('../../hooks', () => ({ - useQueryManager: jest.fn(), -})); - -const mockUseQueryManager = hooks.useQueryManager as jest.MockedFunction; - -// Mock Api singleton -jest.mock('../../singletons', () => ({ - Api: { - instance: { - get: jest.fn(), - post: jest.fn(), - encodePath: jest.fn((path: string) => path), - }, - }, - AppToaster: { - show: jest.fn(), - }, -})); - -// Get mock references after mocking -const mockPost = singletons.Api.instance.post as jest.Mock; -const mockAppToasterShow = singletons.AppToaster.show as jest.Mock; - -// Mock chronoshift Duration -jest.mock('chronoshift', () => ({ - Duration: jest.fn().mockImplementation((period: string) => { - if (period === 'INVALID') { - throw new Error('Invalid period format'); - } - return { - shift: jest.fn((date: Date) => new Date(date.getTime() - 7 * 24 * 60 * 60 * 1000)), - }; - }), - Timezone: { - UTC: 'UTC', - }, -})); - -// Sample test data -const mockTimelineData = { - dataSource: 'test-datasource', - referenceTime: '2024-11-15T12:00:00.000Z', - intervals: [ - { - interval: '2024-11-01T00:00:00.000Z/2024-11-08T00:00:00.000Z', - ruleCount: 3, - config: { - granularitySpec: { - segmentGranularity: 'DAY', - queryGranularity: 'HOUR', - rollup: true, - }, - metricsSpec: [{ type: 'count', name: 'count' }], - }, - appliedRules: [ - { type: 'dataSchema', id: 'schema-1' }, - { type: 'segmentGranularity', id: 'gran-1' }, - { type: 'tuningConfig', id: 'tuning-1' }, - ], - }, - { - interval: '2024-11-08T00:00:00.000Z/2024-11-15T00:00:00.000Z', - ruleCount: 0, - config: {}, - appliedRules: [], - }, - ], -}; - -describe('ReindexingTimeline', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - // ==================================================================== - // PHASE 1: UTILITY FUNCTIONS - // ==================================================================== - describe('utility functions', () => { - // Note: These are internal functions, so we'll test them indirectly through component behavior - // For direct testing, we'd need to export them, which may not be desired - - describe('date formatting', () => { - it('formats dates consistently across the component', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ data: mockTimelineData }), - {} as any, - ]); - - const { container } = render(); - - // Reference time should be formatted with UTC - expect(container.textContent).toContain('Nov 15, 2024'); - }); - }); - - describe('interval color generation', () => { - it('applies different colors to different intervals', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ data: mockTimelineData }), - {} as any, - ]); - - const { container } = render(); - - const segments = container.querySelectorAll('.timeline-segment'); - expect(segments.length).toBe(2); - - // Each segment should have a background color - segments.forEach(segment => { - const htmlSegment = segment as HTMLElement; - expect(htmlSegment.style.backgroundColor).toBeTruthy(); - }); - }); - }); - }); - - // ==================================================================== - // PHASE 2: COMPONENT RENDERING - // ==================================================================== - describe('component rendering', () => { - it('matches snapshot for loading state', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ loading: true }), {} as any]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot for error state', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ error: new Error('Failed to load timeline') }), - {} as any, - ]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot for empty intervals', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - intervals: [], - }, - }), - {} as any, - ]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot with validation error', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - intervals: [], - validationError: { - errorType: 'INVALID_GRANULARITY_TIMELINE', - message: 'Granularity must become coarser over time', - olderInterval: '2024-01-01/2024-02-01', - olderGranularity: 'HOUR', - newerInterval: '2024-02-01/2024-03-01', - newerGranularity: 'DAY', - }, - }, - }), - {} as any, - ]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot with normal intervals', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot with skip offset applied', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - applied: { - type: 'skipOffsetFromNow', - period: 'P7D', - effectiveEndTime: '2024-11-08T12:00:00.000Z', - }, - }, - }, - }), - {} as any, - ]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('matches snapshot with skip offset not applied', () => { - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - notApplied: { - type: 'skipOffsetFromLatest', - period: 'P7D', - reason: 'Requires actual segment timeline data', - }, - }, - }, - }), - {} as any, - ]); - - const { container } = render(); - expect(container.firstChild).toMatchSnapshot(); - }); - - it('marks skipped intervals with special styling', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - - const segments = container.querySelectorAll('.timeline-segment'); - const skippedSegments = container.querySelectorAll('.timeline-segment.skipped'); - - expect(segments.length).toBe(2); - expect(skippedSegments.length).toBe(1); // Second interval has ruleCount: 0 - }); - - it('displays interval details when interval is selected', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - - // Initially no detail panel - expect(container.querySelector('.interval-detail-panel')).toBeNull(); - - // Click first interval - const firstSegment = container.querySelector('.timeline-segment:not(.skipped)')!; - fireEvent.click(firstSegment); - - // Detail panel should appear - expect(container.querySelector('.interval-detail-panel')).toBeTruthy(); - }); - }); - - // ==================================================================== - // PHASE 3: ERROR HANDLING - // ==================================================================== - describe('error handling', () => { - it('handles invalid Duration period gracefully', () => { - const mockDuration = jest.requireMock('chronoshift').Duration; - mockDuration.mockImplementationOnce(() => { - throw new Error('Invalid ISO 8601 duration'); - }); - - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - notApplied: { - type: 'skipOffsetFromLatest', - period: 'INVALID', - reason: 'Test', - }, - }, - }, - }), - {} as any, - ]); - - // Should not crash - const { container } = render(); - expect(container).toBeTruthy(); - }); - - it('handles API errors in handleQueryMaxTime', async () => { - mockPost.mockRejectedValueOnce(new Error('Network error')); - - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - notApplied: { - type: 'skipOffsetFromLatest', - period: 'P7D', - reason: 'Test', - }, - }, - }, - }), - {} as any, - ]); - - const { getByText } = render(); - - const button = getByText('Query latest timestamp'); - fireEvent.click(button); - - // Wait for async operation - await new Promise(resolve => setTimeout(resolve, 100)); - - // Should show error toast - expect(mockAppToasterShow).toHaveBeenCalledWith( - expect.objectContaining({ - intent: 'danger', - }), - ); - }); - - it('handles empty response from timeBoundary query', async () => { - mockPost.mockResolvedValueOnce({ data: [] }); - - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - notApplied: { - type: 'skipOffsetFromLatest', - period: 'P7D', - reason: 'Test', - }, - }, - }, - }), - {} as any, - ]); - - const { getByText } = render(); - - const button = getByText('Query latest timestamp'); - fireEvent.click(button); - - // Wait for async operation - await new Promise(resolve => setTimeout(resolve, 100)); - - // Should show warning toast - expect(mockAppToasterShow).toHaveBeenCalledWith( - expect.objectContaining({ - intent: 'warning', - message: 'No data found in datasource', - }), - ); - }); - - it('handles missing result in timeBoundary response', async () => { - mockPost.mockResolvedValueOnce({ - data: [{ result: null }], - }); - - mockUseQueryManager.mockReturnValue([ - new QueryState({ - data: { - ...mockTimelineData, - skipOffset: { - notApplied: { - type: 'skipOffsetFromLatest', - period: 'P7D', - reason: 'Test', - }, - }, - }, - }), - {} as any, - ]); - - const { getByText } = render(); - - const button = getByText('Query latest timestamp'); - fireEvent.click(button); - - // Wait for async operation - await new Promise(resolve => setTimeout(resolve, 100)); - - // Should show warning toast - expect(mockAppToasterShow).toHaveBeenCalledWith( - expect.objectContaining({ - intent: 'warning', - }), - ); - }); - }); - - // ==================================================================== - // ACCESSIBILITY - // ==================================================================== - describe('accessibility', () => { - it('has proper ARIA attributes on interactive intervals', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - - const interactiveSegment = container.querySelector('.timeline-segment:not(.skipped)')!; - - expect(interactiveSegment.getAttribute('role')).toBe('button'); - expect(interactiveSegment.getAttribute('tabindex')).toBe('0'); - expect(interactiveSegment.getAttribute('aria-label')).toBeTruthy(); - }); - - it('does not make skipped intervals interactive', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - - const skippedSegment = container.querySelector('.timeline-segment.skipped')!; - - expect(skippedSegment.getAttribute('role')).toBeNull(); - expect(skippedSegment.getAttribute('tabindex')).toBeNull(); - }); - - it('has toolbar role on timeline container', () => { - mockUseQueryManager.mockReturnValue([new QueryState({ data: mockTimelineData }), {} as any]); - - const { container } = render(); - - const timeline = container.querySelector('.timeline-bar'); - expect(timeline?.getAttribute('role')).toBe('toolbar'); - expect(timeline?.getAttribute('aria-label')).toBeTruthy(); - }); - }); -}); diff --git a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx b/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx deleted file mode 100644 index dd0ad357d837..000000000000 --- a/web-console/src/components/reindexing-timeline/reindexing-timeline.tsx +++ /dev/null @@ -1,690 +0,0 @@ -/* - * 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. - */ - -import { Button, Callout, Card, Dialog, Intent, Tag, Tooltip } from '@blueprintjs/core'; -import { IconNames } from '@blueprintjs/icons'; -import { Duration, Timezone } from 'chronoshift'; -import { format as formatDate } from 'date-fns'; -import * as JSONBig from 'json-bigint-native'; -import React, { useState } from 'react'; -import AceEditor from 'react-ace'; - -import { useQueryManager } from '../../hooks'; -import { Api, AppToaster } from '../../singletons'; -import { downloadFile } from '../../utils'; -import { Loader } from '../loader/loader'; - -import './reindexing-timeline.scss'; - -const TIMELINE_INTERVAL_COLORS = [ - '#5C7080', // gray - '#738694', // light gray - '#8A9BA8', // lighter gray - '#394B59', // dark gray - '#4A5568', // medium dark gray - '#6B7E91', // medium gray - '#2F343C', // darker gray -]; - -const SKIPPED_INTERVAL_COLOR = 'rgba(219, 55, 55, 0.15)'; -const JSON_VIEWER_HEIGHT = '500px'; - -function getIntervalColor(index: number): string { - return TIMELINE_INTERVAL_COLORS[index % TIMELINE_INTERVAL_COLORS.length]; -} - -interface ReindexingTimelineProps { - supervisorId: string; -} - -interface DimFilter { - type: string; - field?: DimFilter; - fields?: DimFilter[]; -} - -interface TransformSpec { - filter?: DimFilter; - virtualColumns?: any; -} - -interface GranularitySpec { - segmentGranularity?: string; - queryGranularity?: string; - rollup?: boolean; -} - -interface CompactionConfig { - granularitySpec?: GranularitySpec; - metricsSpec?: any[]; - dimensionsSpec?: { - dimensions?: any[]; - }; - projections?: any[]; - transformSpec?: TransformSpec; - tuningConfig?: any; - ioConfig?: any; -} - -interface ReindexingRule { - type: string; - id?: string; - olderThan?: string; - [key: string]: any; -} - -interface IntervalConfig { - interval: string; - ruleCount: number; - config: CompactionConfig; - appliedRules: ReindexingRule[]; -} - -interface SkipOffsetInfo { - applied?: { - type: string; - period: string; - effectiveEndTime: string; - }; - notApplied?: { - type: string; - period: string; - reason: string; - }; -} - -interface ValidationError { - errorType: string; - message: string; - olderInterval?: string; - olderGranularity?: string; - newerInterval?: string; - newerGranularity?: string; -} - -interface ReindexingTimelineData { - dataSource: string; - referenceTime: string; - skipOffset?: SkipOffsetInfo; - intervals: IntervalConfig[]; - validationError?: ValidationError; -} - -export const ReindexingTimeline = React.memo(function ReindexingTimeline( - props: ReindexingTimelineProps, -) { - const { supervisorId } = props; - const [selectedIntervalIndex, setSelectedIntervalIndex] = useState(); - const [queriedMaxTime, setQueriedMaxTime] = useState(); - const [queryingMaxTime, setQueryingMaxTime] = useState(false); - - const [timelineState] = useQueryManager({ - query: supervisorId, - processQuery: async (supervisorId, signal) => { - const resp = await Api.instance.get( - `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}/reindexingTimeline`, - { signal }, - ); - return resp.data; - }, - }); - - if (timelineState.loading) { - return ; - } - - if (timelineState.error) { - return ( -
    - - {timelineState.getErrorMessage()} - -
    - ); - } - - const timelineData = timelineState.data; - if (!timelineData) { - return null; - } - - const { intervals, skipOffset, referenceTime, validationError } = timelineData; - - const handleQueryMaxTime = async () => { - setQueryingMaxTime(true); - try { - const query = { - queryType: 'timeBoundary', - dataSource: timelineData.dataSource, - }; - const resp = await Api.instance.post('/druid/v2', query); - const result = resp.data; - if (result && result.length > 0 && result[0].result) { - const maxTime = result[0].result.maxTime; - setQueriedMaxTime(maxTime); - } else { - AppToaster.show({ - message: 'No data found in datasource', - intent: Intent.WARNING, - }); - } - } catch (e) { - AppToaster.show({ - message: `Failed to query max time: ${e.message}`, - intent: Intent.DANGER, - }); - } finally { - setQueryingMaxTime(false); - } - }; - - // Calculate effective end time if we have queried max time and skipOffsetFromLatest - let effectiveEndTime: Date | undefined; - if (queriedMaxTime && skipOffset?.notApplied) { - const period = skipOffset.notApplied.period; - try { - const duration = new Duration(period); - effectiveEndTime = duration.shift(new Date(queriedMaxTime), Timezone.UTC, -1); - } catch (e) { - console.error('Failed to parse skip offset period:', period, e); - AppToaster.show({ - message: `Invalid skip offset period format: ${period}`, - intent: Intent.WARNING, - }); - } - } - - // Display validation error if present - if (validationError) { - return ( -
    - -

    - - The segment granularity rule definitions have created an illegal segment granularity - timeline. - -

    - {validationError.errorType === 'INVALID_GRANULARITY_TIMELINE' && - validationError.olderInterval && ( - <> -

    - Segment granularity must stay the same or become coarser as data - ages from present to past. Your configuration violates this constraint: -

    -
      -
    • - Older interval (further in the past):{' '} - {formatInterval(validationError.olderInterval)} has{' '} - finer granularity:{' '} - {validationError.olderGranularity} -
    • -
    • - Newer interval (more recent):{' '} - {formatInterval(validationError.newerInterval!)} has{' '} - coarser granularity:{' '} - {validationError.newerGranularity} -
    • -
    -

    - To fix this: Adjust your segment granularity rules so that as - time moves from present to past, the granularity either stays the same or gets - coarser (e.g., HOUR → DAY → MONTH → YEAR). -

    - - )} - {validationError.errorType !== 'INVALID_GRANULARITY_TIMELINE' && ( -

    {validationError.message}

    - )} -
    -
    - ); - } - - if (intervals.length === 0) { - return ( -
    - - No reindexing intervals found. This may indicate that the rule provider is not ready or no - rules are configured. - -
    - ); - } - - const selectedInterval = - selectedIntervalIndex !== undefined ? intervals[selectedIntervalIndex] : undefined; - - return ( -
    -
    -
    - DataSource: {timelineData.dataSource} - | - - - Reference Time: - - {' '} - {formatDateTimeUTC(referenceTime)} -
    - {skipOffset && ( -
    - {skipOffset.applied && ( - - Skip Offset: {skipOffset.applied.type} ({skipOffset.applied.period}) - - )} - {skipOffset.notApplied && !queriedMaxTime && ( - <> - - - {skipOffset.notApplied.type} ({skipOffset.notApplied.period}): Not reflected in - this preview - - -
    - )} -
    - - -
    - {intervals.map((interval, idx) => { - const [start, end] = interval.interval.split('/'); - const isSelected = selectedIntervalIndex === idx; - - // Check if interval is skipped due to skip offset (either from API or from queried max time) - let isSkipped = interval.ruleCount === 0; - if (!isSkipped && effectiveEndTime) { - const intervalEnd = new Date(end); - isSkipped = intervalEnd > effectiveEndTime; - } - - return ( -
    !isSkipped && setSelectedIntervalIndex(idx)} - onKeyDown={e => { - if (!isSkipped && (e.key === 'Enter' || e.key === ' ')) { - e.preventDefault(); - setSelectedIntervalIndex(idx); - } - }} - title={ - isSkipped - ? `${interval.interval}\nSkipped (beyond skip offset)` - : `${interval.interval}\n${interval.ruleCount} rule(s) applied` - } - > -
    -
    {formatDateShort(start)}
    -
    {formatDateShort(end)}
    -
    - - {isSkipped - ? 'skipped' - : `${interval.ruleCount} rule${interval.ruleCount !== 1 ? 's' : ''}`} - -
    -
    -
    - ); - })} -
    -
    Click on an interval to view its configuration details
    -
    - - {selectedInterval && selectedInterval.ruleCount > 0 && ( - setSelectedIntervalIndex(undefined)} - /> - )} -
    - ); -}); - -function formatDateShort(isoDate: string): string { - // Handle start of time / very old dates - if (isoDate.startsWith('-')) { - return '-INF'; - } - - // Format: "Feb 15, 2026" - return formatDate(new Date(isoDate), 'MMM d, yyyy'); -} - -function formatDateTimeUTC(isoDate: string): string { - // Handle start of time / very old dates - if (isoDate.startsWith('-')) { - return '-INF'; - } - - // Format: "Feb 15, 2026 3:45 PM UTC" - return formatDate(new Date(isoDate), "MMM d, yyyy h:mm a 'UTC'"); -} - -function formatInterval(interval: string): string { - const [start, end] = interval.split('/'); - const formattedStart = start.startsWith('-') ? '-INF' : start; - const formattedEnd = end ? end : 'now'; - return `${formattedStart}/${formattedEnd}`; -} - -function handleCopyToClipboard(data: CompactionConfig | ReindexingRule[], label: string): void { - const jsonValue = JSONBig.stringify(data, undefined, 2); - void navigator.clipboard.writeText(jsonValue); - AppToaster.show({ - message: `${label} copied to clipboard`, - intent: Intent.SUCCESS, - }); -} - -function handleDownloadJson(data: CompactionConfig | ReindexingRule[], filename: string): void { - const jsonValue = JSONBig.stringify(data, undefined, 2); - downloadFile(jsonValue, 'json', filename); -} - -interface IntervalDetailPanelProps { - interval: IntervalConfig; - onClose: () => void; -} - -function IntervalDetailPanel({ interval, onClose }: IntervalDetailPanelProps) { - const [showFullConfig, setShowFullConfig] = useState(false); - const [showRawRules, setShowRawRules] = useState(false); - const { config } = interval; - - // Count deletion rules from transform spec - const deletionRuleCount = countDeletionRules(config.transformSpec); - const metricsCount = config.metricsSpec?.length || 0; - const dimensionsCount = config.dimensionsSpec?.dimensions?.length || 0; - const projectionsCount = config.projections?.length || 0; - - return ( - <> - -
    -
    -

    Interval: {formatInterval(interval.interval)}

    - - {interval.ruleCount} rule{interval.ruleCount !== 1 ? 's' : ''} applied - -
    -
    - -
    -
    - {config.granularitySpec?.segmentGranularity && ( - - Segment: {config.granularitySpec.segmentGranularity} - - )} - {config.granularitySpec?.queryGranularity && ( - - Query: {config.granularitySpec.queryGranularity} - - )} - {metricsCount > 0 && ( - - {metricsCount} metric{metricsCount !== 1 ? 's' : ''} - - )} - {dimensionsCount > 0 && ( - - {dimensionsCount} dimension{dimensionsCount !== 1 ? 's' : ''} - - )} - {projectionsCount > 0 && ( - - {projectionsCount} projection{projectionsCount !== 1 ? 's' : ''} - - )} - {deletionRuleCount > 0 && ( - - {deletionRuleCount} deletion rule{deletionRuleCount !== 1 ? 's' : ''} - - )} -
    - -
    -
    -
    -
    - - setShowFullConfig(false)} - title={ - - {formatInterval(interval.interval)} - - } - className="reindexing-config-dialog" - canOutsideClickClose - > -
    - -
    -
    -
    -
    -
    -
    - - setShowRawRules(false)} - title={ - - Rules for {formatInterval(interval.interval)} - - } - className="reindexing-config-dialog" - canOutsideClickClose - > -
    - -
    -
    -
    -
    -
    -
    - - ); -} - -function countDeletionRules(transformSpec?: TransformSpec): number { - if (!transformSpec || !transformSpec.filter) { - return 0; - } - - const filter = transformSpec.filter; - - // Check if it's a NotDimFilter with fields - if (filter.type === 'not' && filter.field) { - // If the field is an 'or' filter, count the number of fields in it - if (filter.field.type === 'or' && filter.field.fields) { - return filter.field.fields.length; - } - // Otherwise it's a single deletion rule - return 1; - } - - return 0; -} - -interface ConfigJsonViewerProps { - config: CompactionConfig | ReindexingRule[]; - isRulesList?: boolean; -} - -function ConfigJsonViewer({ config, isRulesList }: ConfigJsonViewerProps) { - let jsonValue: string; - - if (isRulesList && Array.isArray(config)) { - // Group rules by type for better readability - const rulesByType: Record = {}; - config.forEach((rule: ReindexingRule) => { - const type = getRuleTypeName(rule); - if (!rulesByType[type]) { - rulesByType[type] = []; - } - rulesByType[type].push(rule); - }); - jsonValue = JSONBig.stringify(rulesByType, undefined, 2); - } else { - jsonValue = JSONBig.stringify(config, undefined, 2); - } - - return ( -
    - -
    - ); -} - -function getRuleTypeName(rule: ReindexingRule): string { - // Use the explicit type field provided by Jackson serialization - const typeMap: Record = { - deletion: 'Deletion Rules', - dataSchema: 'Data Schema Rules', - segmentGranularity: 'Segment Granularity Rules', - tuningConfig: 'Tuning Config Rules', - ioConfig: 'IO Config Rules', - }; - - return typeMap[rule.type] || 'Other Rules'; -} diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap index f0bede858f21..ee93c8fd6222 100755 --- a/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap +++ b/web-console/src/dialogs/supervisor-table-action-dialog/__snapshots__/supervisor-table-action-dialog.spec.tsx.snap @@ -174,33 +174,6 @@ exports[`SupervisorTableActionDialog matches snapshot 1`] = ` History -
    setActiveTab('history'), - }, - { - icon: 'timeline-events', - text: 'Timeline', - active: activeTab === 'timeline', - onClick: () => setActiveTab('timeline'), - }, + } ]; const supervisorEndpointBase = `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}`; @@ -104,7 +98,6 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc /> )} {activeTab === 'history' && } - {activeTab === 'timeline' && } ); }); From 0d0d0c505d7ab704647dd41b97d08bbe8fd38352 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 17 Feb 2026 14:37:01 -0600 Subject: [PATCH 74/90] Remove support for the reindexing timeline creation from app code --- .../compact/CascadingReindexingTemplate.java | 138 -------- .../supervisor/SupervisorResource.java | 80 ----- .../CascadingReindexingTemplateTest.java | 326 ------------------ .../supervisor/SupervisorResourceTest.java | 129 ------- 4 files changed, 673 deletions(-) diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index eb589b036dbb..2cab05d5ccb7 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -200,32 +200,6 @@ public Granularity getDefaultSegmentGranularity() return defaultSegmentGranularity; } - /** - * Creates a validation error view for timeline generation failures. - * Logs the exception and returns a timeline view containing the validation error details. - */ - private ReindexingTimelineView createValidationErrorView( - Exception e, - DateTime referenceTime, - String errorType, - @Nullable String olderInterval, - @Nullable String olderGranularity, - @Nullable String newerInterval, - @Nullable String newerGranularity - ) - { - LOG.warn(e, "Validation failed for reindexing timeline of dataSource[%s]", dataSource); - ReindexingTimelineView.ValidationError validationError = new ReindexingTimelineView.ValidationError( - errorType, - e.getMessage(), - olderInterval, - olderGranularity, - newerInterval, - newerGranularity - ); - return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), validationError); - } - /** * Checks if the given interval's end time is after the specified boundary. * Used to determine if intervals should be skipped based on skip offset configuration. @@ -239,118 +213,6 @@ private static boolean intervalEndsAfter(Interval interval, DateTime boundary) return interval.getEnd().isAfter(boundary); } - /** - * Generates a timeline view showing the search intervals and their associated reindexing - * configurations. This is useful for operators to understand how rules are applied across - * different time periods and to preview the effects of rule changes before they are applied. - * - * @param referenceTime the reference time to use for computing rule periods (typically DateTime.now()) - * @return a view of the reindexing timeline with intervals and their configs - */ - public ReindexingTimelineView getReindexingTimelineView(DateTime referenceTime) - { - if (!ruleProvider.isReady()) { - LOG.info( - "Rule provider [%s] is not ready, returning empty timeline for dataSource[%s]", - ruleProvider.getType(), - dataSource - ); - return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); - } - - List searchIntervals; - try { - searchIntervals = generateAlignedSearchIntervals(referenceTime); - } - catch (SegmentGranularityTimelineValidationException e) { - return createValidationErrorView( - e, - referenceTime, - "INVALID_GRANULARITY_TIMELINE", - e.getOlderInterval().toString(), - e.getOlderGranularity().toString(), - e.getNewerInterval().toString(), - e.getNewerGranularity().toString() - ); - } - catch (IAE e) { - return createValidationErrorView( - e, - referenceTime, - "VALIDATION_ERROR", - null, - null, - null, - null - ); - } - - if (searchIntervals.isEmpty()) { - LOG.warn("No search intervals generated for dataSource[%s]", dataSource); - return new ReindexingTimelineView(dataSource, referenceTime, null, Collections.emptyList(), null); - } - - // Calculate effective end time based on skip offset - DateTime effectiveEndTime = referenceTime; - ReindexingTimelineView.SkipOffsetInfo skipOffsetInfo = null; - - if (skipOffsetFromNow != null) { - effectiveEndTime = referenceTime.minus(skipOffsetFromNow); - ReindexingTimelineView.AppliedSkipOffset applied = new ReindexingTimelineView.AppliedSkipOffset( - "skipOffsetFromNow", - skipOffsetFromNow, - effectiveEndTime - ); - skipOffsetInfo = new ReindexingTimelineView.SkipOffsetInfo(applied, null); - } else if (skipOffsetFromLatest != null) { - // skipOffsetFromLatest requires actual timeline data, so we can't apply it in preview mode - ReindexingTimelineView.NotAppliedSkipOffset notApplied = new ReindexingTimelineView.NotAppliedSkipOffset( - "skipOffsetFromLatest", - skipOffsetFromLatest, - "Requires actual segment timeline data" - ); - skipOffsetInfo = new ReindexingTimelineView.SkipOffsetInfo(null, notApplied); - } - - // Build configs for each interval - List intervalConfigs = new ArrayList<>(); - for (IntervalGranularityInfo intervalInfo : searchIntervals) { - Interval searchInterval = intervalInfo.getInterval(); - - // Check if interval extends past skip offset - if (intervalEndsAfter(searchInterval, effectiveEndTime)) { - // Include in timeline, but indicate skipped by applying no rules. - intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( - searchInterval, - 0, - null, - Collections.emptyList() - )); - continue; - } - - InlineSchemaDataSourceCompactionConfig.Builder builder = createBaseBuilder(); - ReindexingConfigBuilder configBuilder = new ReindexingConfigBuilder( - ruleProvider, - searchInterval, - referenceTime, - searchIntervals - ); - ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builder); - - if (buildResult.getRuleCount() > 0) { - intervalConfigs.add(new ReindexingTimelineView.IntervalConfig( - searchInterval, - buildResult.getRuleCount(), - builder.build(), - buildResult.getAppliedRules() - )); - } - } - - return new ReindexingTimelineView(dataSource, referenceTime, skipOffsetInfo, intervalConfigs, null); - } - @Override public List createCompactionJobs( DruidInputSource source, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java index 51fe216d9aeb..6b145be07e4a 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResource.java @@ -35,14 +35,9 @@ import org.apache.commons.lang3.NotImplementedException; import org.apache.druid.audit.AuditEntry; import org.apache.druid.audit.AuditManager; -import org.apache.druid.indexing.compact.CascadingReindexingTemplate; -import org.apache.druid.indexing.compact.CompactionJobTemplate; -import org.apache.druid.indexing.compact.CompactionSupervisorSpec; -import org.apache.druid.indexing.compact.ReindexingTimelineView; import org.apache.druid.indexing.overlord.DataSourceMetadata; import org.apache.druid.indexing.overlord.TaskMaster; import org.apache.druid.indexing.overlord.http.security.SupervisorResourceFilter; -import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.StringUtils; import org.apache.druid.java.util.common.UOE; import org.apache.druid.segment.incremental.ParseExceptionReport; @@ -56,7 +51,6 @@ import org.apache.druid.server.security.ResourceAction; import org.apache.druid.server.security.ResourceType; import org.apache.druid.utils.CollectionUtils; -import org.joda.time.DateTime; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -357,80 +351,6 @@ public Response getAllTaskStats( ); } - @GET - @Path("/{id}/reindexingTimeline") - @Produces(MediaType.APPLICATION_JSON) - @ResourceFilters(SupervisorResourceFilter.class) - public Response getReindexingTimeline( - @PathParam("id") final String id, - @QueryParam("referenceTime") @Nullable final String referenceTimeStr - ) - { - return asLeaderWithSupervisorManager( - manager -> { - Optional specOptional = manager.getSupervisorSpec(id); - if (!specOptional.isPresent()) { - return Response.status(Response.Status.NOT_FOUND) - .entity(ImmutableMap.of("error", StringUtils.format("[%s] does not exist", id))) - .build(); - } - - SupervisorSpec spec = specOptional.get(); - if (!(spec instanceof CompactionSupervisorSpec)) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of( - "error", - StringUtils.format( - "[%s] is not a compaction supervisor (type: %s)", - id, - spec.getClass().getSimpleName() - ) - )) - .build(); - } - - CompactionSupervisorSpec compactionSpec = (CompactionSupervisorSpec) spec; - - DateTime referenceTime; - if (referenceTimeStr != null) { - try { - referenceTime = DateTimes.of(referenceTimeStr); - } - catch (IllegalArgumentException e) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of( - "error", - StringUtils.format("Invalid referenceTime format: %s", referenceTimeStr) - )) - .build(); - } - } else { - referenceTime = DateTimes.nowUtc(); - } - - CompactionJobTemplate template = compactionSpec.getTemplate(); - if (!(template instanceof CascadingReindexingTemplate)) { - return Response.status(Response.Status.BAD_REQUEST) - .entity(ImmutableMap.of( - "error", - StringUtils.format( - "Reindexing timeline is only available for cascading reindexing supervisors. " + - "Supervisor [%s] uses template type: %s", - id, - template.getClass().getSimpleName() - ) - )) - .build(); - } - - CascadingReindexingTemplate cascadingTemplate = (CascadingReindexingTemplate) template; - - ReindexingTimelineView timelineView = cascadingTemplate.getReindexingTimelineView(referenceTime); - return Response.ok(timelineView).build(); - } - ); - } - @GET @Path("/{id}/parseErrors") @Produces(MediaType.APPLICATION_JSON) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 3cb3b8223ce0..8a805a5a9212 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -29,23 +29,14 @@ import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.AggregatorFactory; -import org.apache.druid.query.aggregation.CountAggregatorFactory; -import org.apache.druid.query.filter.EqualityFilter; -import org.apache.druid.segment.column.ColumnType; import org.apache.druid.server.compaction.InlineReindexingRuleProvider; import org.apache.druid.server.compaction.IntervalGranularityInfo; import org.apache.druid.server.compaction.ReindexingDataSchemaRule; -import org.apache.druid.server.compaction.ReindexingDeletionRule; -import org.apache.druid.server.compaction.ReindexingIOConfigRule; import org.apache.druid.server.compaction.ReindexingRule; import org.apache.druid.server.compaction.ReindexingRuleProvider; import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; -import org.apache.druid.server.compaction.ReindexingTuningConfigRule; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; -import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.apache.druid.testing.InitializedNullHandlingTest; import org.apache.druid.timeline.DataSegment; import org.apache.druid.timeline.SegmentTimeline; @@ -1307,323 +1298,6 @@ public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranul ); } - /** - * Comprehensive test covering: - * - Multiple intervals with different segment granularities - * - All rule types (segment gran, data schema, deletion, tuning, IO) - * - Non-segment-gran rules triggering interval splitting - * - Applied rules tracking with correct rule types in each interval - * - Full DataSourceCompactionConfig generation - * - Rule count accuracy - */ - @Test - public void test_getReindexingTimelineView_comprehensive() - { - DateTime referenceTime = DateTimes.of("2025-02-01T00:00:00Z"); - - // Create rules with various periods to test interval generation and splitting - ReindexingSegmentGranularityRule segGran7d = new ReindexingSegmentGranularityRule( - "seg-gran-7d", - null, - Period.days(7), - Granularities.HOUR - ); - - ReindexingSegmentGranularityRule segGran30d = new ReindexingSegmentGranularityRule( - "seg-gran-30d", - null, - Period.days(30), - Granularities.DAY - ); - - // Data schema rule at P15D (will split the HOUR interval) - ReindexingDataSchemaRule dataSchema15d = new ReindexingDataSchemaRule( - "data-schema-15d", - null, - Period.days(15), - new UserCompactionTaskDimensionsConfig(null), - new AggregatorFactory[]{new CountAggregatorFactory("count")}, - Granularities.MINUTE, - true, - null - ); - - // Deletion rules at different periods - ReindexingDeletionRule deletion10d = new ReindexingDeletionRule( - "deletion-10d", - null, - Period.days(10), - new EqualityFilter("country", ColumnType.STRING, "US", null), - null - ); - - ReindexingDeletionRule deletion20d = new ReindexingDeletionRule( - "deletion-20d", - null, - Period.days(20), - new EqualityFilter("device", ColumnType.STRING, "mobile", null), - null - ); - - // Tuning and IO rules - ReindexingTuningConfigRule tuning7d = new ReindexingTuningConfigRule( - "tuning-7d", - null, - Period.days(7), - new UserCompactionTaskQueryTuningConfig( - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null - ) - ); - - ReindexingIOConfigRule io7d = new ReindexingIOConfigRule( - "io-7d", - null, - Period.days(7), - new UserCompactionTaskIOConfig(true) - ); - - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of(segGran7d, segGran30d)) - .dataSchemaRules(List.of(dataSchema15d)) - .deletionRules(List.of(deletion10d, deletion20d)) - .tuningConfigRules(List.of(tuning7d)) - .ioConfigRules(List.of(io7d)) - .build(); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - provider, - null, - null, - null, - null, - Granularities.DAY - ); - - ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); - - // Verify basic timeline properties - Assert.assertEquals("testDS", timeline.getDataSource()); - Assert.assertEquals(referenceTime, timeline.getReferenceTime()); - Assert.assertNull(timeline.getValidationError()); - Assert.assertNull(timeline.getSkipOffset()); - - // Verify we have multiple intervals (splitting should occur) - Assert.assertTrue("Expected at least 2 intervals", timeline.getIntervals().size() >= 2); - - // Verify each interval has correct structure - for (ReindexingTimelineView.IntervalConfig intervalConfig : timeline.getIntervals()) { - Assert.assertNotNull(intervalConfig.getInterval()); - Assert.assertTrue("Rule count should be > 0", intervalConfig.getRuleCount() > 0); - Assert.assertNotNull(intervalConfig.getConfig()); - Assert.assertNotNull(intervalConfig.getAppliedRules()); - Assert.assertEquals( - "Applied rules size should match rule count", - intervalConfig.getRuleCount(), - intervalConfig.getAppliedRules().size() - ); - - // Verify config has expected components - DataSourceCompactionConfig config = intervalConfig.getConfig(); - Assert.assertNotNull("Should have granularity spec", config.getGranularitySpec()); - Assert.assertNotNull("Should have segment granularity", config.getGranularitySpec().getSegmentGranularity()); - - // Verify appliedRules contain expected rule types - boolean hasTuningRule = intervalConfig.getAppliedRules().stream() - .anyMatch(r -> r instanceof ReindexingTuningConfigRule); - boolean hasIORule = intervalConfig.getAppliedRules().stream() - .anyMatch(r -> r instanceof ReindexingIOConfigRule); - boolean hasDataSchemaRule = intervalConfig.getAppliedRules().stream() - .anyMatch(r -> r instanceof ReindexingDataSchemaRule); - boolean hasDeletionRule = intervalConfig.getAppliedRules().stream() - .anyMatch(r -> r instanceof ReindexingDeletionRule); - boolean hasSegmentGranRule = intervalConfig.getAppliedRules().stream() - .anyMatch(r -> r instanceof ReindexingSegmentGranularityRule); - - // Most recent intervals should have more rules applied - if (intervalConfig.getInterval().getEnd().isAfter(referenceTime.minusDays(10))) { - Assert.assertTrue("Recent intervals should have tuning rules", hasTuningRule); - Assert.assertTrue("Recent intervals should have IO rules", hasIORule); - } - } - } - - /** - * Test that skipOffsetFromNow correctly skips intervals and populates skipOffset.applied - */ - @Test - public void test_getReindexingTimelineView_skipOffsetFromNow_skipsProperIntervals() - { - DateTime referenceTime = DateTimes.of("2025-01-29T00:00:00Z"); - Period skipOffset = Period.days(10); - - // Create rules where the most recent rule has a period SMALLER than the skip offset - // This ensures the interval would extend beyond the effectiveEndTime and get clamped - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("seg-3d", null, Period.days(3), Granularities.HOUR), - new ReindexingSegmentGranularityRule("seg-30d", null, Period.days(30), Granularities.DAY) - )) - .build(); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - provider, - null, - null, - null, - skipOffset, // skipOffsetFromNow - Granularities.DAY - ); - - ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); - - // Verify skipOffset is applied - Assert.assertNotNull("Skip offset should be present", timeline.getSkipOffset()); - Assert.assertNotNull("Skip offset should be applied", timeline.getSkipOffset().getApplied()); - Assert.assertNull("Skip offset notApplied should be null", timeline.getSkipOffset().getNotApplied()); - - ReindexingTimelineView.AppliedSkipOffset applied = timeline.getSkipOffset().getApplied(); - Assert.assertEquals("skipOffsetFromNow", applied.getType()); - Assert.assertEquals(skipOffset, applied.getPeriod()); - - DateTime expectedEffectiveEndTime = referenceTime.minus(skipOffset); - Assert.assertEquals(expectedEffectiveEndTime, applied.getEffectiveEndTime()); - - for (ReindexingTimelineView.IntervalConfig intervalConfig : timeline.getIntervals()) { - if (intervalConfig.getInterval().getEnd().isAfter(expectedEffectiveEndTime)) { - Assert.assertEquals(0, intervalConfig.getRuleCount()); - } - } - } - - /** - * Test validation error when granularity timeline is invalid - */ - @Test - public void test_getReindexingTimelineView_validationError_invalidGranularityTimeline() - { - DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - - // Create rules that violate the granularity constraint: - // Older data (P90D) has DAY granularity, newer data (P30D) has HOUR granularity - // This means as we move from past to present, granularity gets finer (valid) - // But then if we add MONTH for recent data, it becomes coarser (invalid) - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), - new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) - )) - .dataSchemaRules(List.of( - // This will trigger prepending an interval with default granularity (MONTH) - // which is coarser than HOUR, causing validation failure - new ReindexingDataSchemaRule( - "metrics-7d", - null, - Period.days(7), - null, - new AggregatorFactory[]{new CountAggregatorFactory("count")}, - null, - null, - null - ) - )) - .build(); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - provider, - null, - null, - null, - null, - Granularities.MONTH // This is coarser than HOUR, will cause validation error - ); - - ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); - - // Verify validation error is present - Assert.assertNotNull("Validation error should be present", timeline.getValidationError()); - Assert.assertEquals( - "INVALID_GRANULARITY_TIMELINE", - timeline.getValidationError().getErrorType() - ); - Assert.assertNotNull(timeline.getValidationError().getMessage()); - Assert.assertTrue( - timeline.getValidationError().getMessage().contains("Invalid segment granularity timeline") - ); - - // Verify structured error information is populated - Assert.assertNotNull(timeline.getValidationError().getOlderInterval()); - Assert.assertNotNull(timeline.getValidationError().getOlderGranularity()); - Assert.assertNotNull(timeline.getValidationError().getNewerInterval()); - Assert.assertNotNull(timeline.getValidationError().getNewerGranularity()); - - // Verify intervals list is empty when validation fails - Assert.assertTrue("Intervals should be empty on validation error", timeline.getIntervals().isEmpty()); - } - - /** - * Test graceful handling when rule provider is not ready - */ - @Test - public void test_getReindexingTimelineView_ruleProviderNotReady() - { - DateTime referenceTime = DateTimes.of("2025-01-29T00:00:00Z"); - - // Create a mock provider that is not ready - ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); - EasyMock.expect(mockProvider.isReady()).andReturn(false).anyTimes(); - EasyMock.expect(mockProvider.getType()).andReturn("mock").anyTimes(); - EasyMock.replay(mockProvider); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - mockProvider, - null, - null, - null, - null, - Granularities.DAY - ); - - ReindexingTimelineView timeline = template.getReindexingTimelineView(referenceTime); - - // Verify timeline is returned with empty intervals - Assert.assertNotNull(timeline); - Assert.assertEquals("testDS", timeline.getDataSource()); - Assert.assertEquals(referenceTime, timeline.getReferenceTime()); - Assert.assertTrue("Intervals should be empty", timeline.getIntervals().isEmpty()); - Assert.assertNull("No validation error", timeline.getValidationError()); - Assert.assertNull("No skip offset", timeline.getSkipOffset()); - } - private DruidInputSource createMockSource() { final Interval[] capturedInterval = new Interval[1]; diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java index e43eb52092d4..6c099a53b3a1 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/overlord/supervisor/SupervisorResourceTest.java @@ -26,10 +26,6 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.druid.audit.AuditManager; -import org.apache.druid.indexing.compact.CascadingReindexingTemplate; -import org.apache.druid.indexing.compact.CompactionJobTemplate; -import org.apache.druid.indexing.compact.CompactionSupervisorSpec; -import org.apache.druid.indexing.compact.ReindexingTimelineView; import org.apache.druid.indexing.overlord.DataSourceMetadata; import org.apache.druid.indexing.overlord.IndexerMetadataStorageCoordinator; import org.apache.druid.indexing.overlord.TaskMaster; @@ -1400,131 +1396,6 @@ public void test_handoffTaskGroups_returnsAccepted() verifyAll(); } - @Test - public void testGetReindexingTimeline_success() - { - CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); - CascadingReindexingTemplate template = EasyMock.createMock(CascadingReindexingTemplate.class); - ReindexingTimelineView timelineView = EasyMock.createMock(ReindexingTimelineView.class); - - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); - EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); - EasyMock.expect(template.getReindexingTimelineView(EasyMock.anyObject())).andReturn(timelineView); - EasyMock.replay(compactionSpec, template, timelineView); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("compaction-id", null); - - Assert.assertEquals(200, response.getStatus()); - Assert.assertEquals(timelineView, response.getEntity()); - verifyAll(); - EasyMock.verify(compactionSpec, template, timelineView); - } - - @Test - public void testGetReindexingTimeline_supervisorNotFound() - { - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("missing-id")).andReturn(Optional.absent()); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("missing-id", null); - - Assert.assertEquals(404, response.getStatus()); - verifyAll(); - } - - @Test - public void testGetReindexingTimeline_notCompactionSupervisor() - { - TestSupervisorSpec regularSpec = new TestSupervisorSpec("regular-id", null, null); - - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("regular-id")).andReturn(Optional.of(regularSpec)); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("regular-id", null); - - Assert.assertEquals(400, response.getStatus()); - Map entity = (Map) response.getEntity(); - Assert.assertTrue(entity.get("error").toString().contains("not a compaction supervisor")); - verifyAll(); - } - - @Test - public void testGetReindexingTimeline_notCascadingReindexingTemplate() - { - CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); - CompactionJobTemplate template = EasyMock.createMock(CompactionJobTemplate.class); - - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); - EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); - EasyMock.replay(compactionSpec, template); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("compaction-id", null); - - Assert.assertEquals(400, response.getStatus()); - Map entity = (Map) response.getEntity(); - Assert.assertTrue(entity.get("error").toString().contains("only available for cascading reindexing supervisors")); - verifyAll(); - EasyMock.verify(compactionSpec, template); - } - - @Test - public void testGetReindexingTimeline_withReferenceTime() - { - CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); - CascadingReindexingTemplate template = EasyMock.createMock(CascadingReindexingTemplate.class); - ReindexingTimelineView timelineView = EasyMock.createMock(ReindexingTimelineView.class); - - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); - EasyMock.expect(compactionSpec.getTemplate()).andReturn(template); - EasyMock.expect(template.getReindexingTimelineView(EasyMock.anyObject())).andReturn(timelineView); - EasyMock.replay(compactionSpec, template, timelineView); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("compaction-id", "2024-01-01T00:00:00.000Z"); - - Assert.assertEquals(200, response.getStatus()); - verifyAll(); - EasyMock.verify(compactionSpec, template, timelineView); - } - - @Test - public void testGetReindexingTimeline_invalidReferenceTimeFormat() - { - CompactionSupervisorSpec compactionSpec = EasyMock.createMock(CompactionSupervisorSpec.class); - - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.of(supervisorManager)); - EasyMock.expect(supervisorManager.getSupervisorSpec("compaction-id")).andReturn(Optional.of(compactionSpec)); - EasyMock.replay(compactionSpec); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("compaction-id", "not-a-date"); - - Assert.assertEquals(400, response.getStatus()); - Map entity = (Map) response.getEntity(); - Assert.assertTrue(entity.get("error").toString().contains("Invalid referenceTime format")); - verifyAll(); - EasyMock.verify(compactionSpec); - } - - @Test - public void testGetReindexingTimeline_serviceUnavailable() - { - EasyMock.expect(taskMaster.getSupervisorManager()).andReturn(Optional.absent()); - replayAll(); - - Response response = supervisorResource.getReindexingTimeline("any-id", null); - - Assert.assertEquals(503, response.getStatus()); - verifyAll(); - } - private TestSeekableStreamSupervisorSpec createTestSpec(Integer taskCount, int taskCountMin) { HashMap autoScalerConfig = new HashMap<>(); From 4b211a040512ebca019342dc388e2435167e2c93 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 17 Feb 2026 14:50:41 -0600 Subject: [PATCH 75/90] missed a few spots --- .../compact/ReindexingTimelineView.java | 482 ------------------ .../supervisor-table-action-dialog.tsx | 2 +- 2 files changed, 1 insertion(+), 483 deletions(-) delete mode 100644 indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java deleted file mode 100644 index a6f79dbb027c..000000000000 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/ReindexingTimelineView.java +++ /dev/null @@ -1,482 +0,0 @@ -/* - * 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.indexing.compact; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSubTypes; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import org.apache.druid.server.compaction.ReindexingDataSchemaRule; -import org.apache.druid.server.compaction.ReindexingDeletionRule; -import org.apache.druid.server.compaction.ReindexingIOConfigRule; -import org.apache.druid.server.compaction.ReindexingRule; -import org.apache.druid.server.compaction.ReindexingSegmentGranularityRule; -import org.apache.druid.server.compaction.ReindexingTuningConfigRule; -import org.apache.druid.server.coordinator.DataSourceCompactionConfig; -import org.joda.time.DateTime; -import org.joda.time.Interval; -import org.joda.time.Period; - -import javax.annotation.Nullable; -import java.util.List; -import java.util.Objects; - -/** - * Represents the timeline of search intervals and their associated reindexing configurations - * for a cascading reindexing supervisor. This view helps operators understand how different - * rules are applied across time intervals. - */ -public class ReindexingTimelineView -{ - private final String dataSource; - private final DateTime referenceTime; - private final SkipOffsetInfo skipOffset; - private final List intervals; - private final ValidationError validationError; - - @JsonCreator - public ReindexingTimelineView( - @JsonProperty("dataSource") String dataSource, - @JsonProperty("referenceTime") DateTime referenceTime, - @JsonProperty("skipOffset") @Nullable SkipOffsetInfo skipOffset, - @JsonProperty("intervals") List intervals, - @JsonProperty("validationError") @Nullable ValidationError validationError - ) - { - this.dataSource = dataSource; - this.referenceTime = referenceTime; - this.skipOffset = skipOffset; - this.intervals = intervals; - this.validationError = validationError; - } - - @JsonProperty - public String getDataSource() - { - return dataSource; - } - - @JsonProperty - public DateTime getReferenceTime() - { - return referenceTime; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public SkipOffsetInfo getSkipOffset() - { - return skipOffset; - } - - @JsonProperty - public List getIntervals() - { - return intervals; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public ValidationError getValidationError() - { - return validationError; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ReindexingTimelineView that = (ReindexingTimelineView) o; - return Objects.equals(dataSource, that.dataSource) && - Objects.equals(referenceTime, that.referenceTime) && - Objects.equals(skipOffset, that.skipOffset) && - Objects.equals(intervals, that.intervals) && - Objects.equals(validationError, that.validationError); - } - - @Override - public int hashCode() - { - return Objects.hash(dataSource, referenceTime, skipOffset, intervals, validationError); - } - - /** - * Information about a validation error that occurred while building the timeline. - */ - public static class ValidationError - { - private final String errorType; - private final String message; - private final String olderInterval; - private final String olderGranularity; - private final String newerInterval; - private final String newerGranularity; - - @JsonCreator - public ValidationError( - @JsonProperty("errorType") String errorType, - @JsonProperty("message") String message, - @JsonProperty("olderInterval") @Nullable String olderInterval, - @JsonProperty("olderGranularity") @Nullable String olderGranularity, - @JsonProperty("newerInterval") @Nullable String newerInterval, - @JsonProperty("newerGranularity") @Nullable String newerGranularity - ) - { - this.errorType = errorType; - this.message = message; - this.olderInterval = olderInterval; - this.olderGranularity = olderGranularity; - this.newerInterval = newerInterval; - this.newerGranularity = newerGranularity; - } - - @JsonProperty - public String getErrorType() - { - return errorType; - } - - @JsonProperty - public String getMessage() - { - return message; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public String getOlderInterval() - { - return olderInterval; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public String getOlderGranularity() - { - return olderGranularity; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public String getNewerInterval() - { - return newerInterval; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public String getNewerGranularity() - { - return newerGranularity; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - ValidationError that = (ValidationError) o; - return Objects.equals(errorType, that.errorType) && - Objects.equals(message, that.message) && - Objects.equals(olderInterval, that.olderInterval) && - Objects.equals(olderGranularity, that.olderGranularity) && - Objects.equals(newerInterval, that.newerInterval) && - Objects.equals(newerGranularity, that.newerGranularity); - } - - @Override - public int hashCode() - { - return Objects.hash(errorType, message, olderInterval, olderGranularity, newerInterval, newerGranularity); - } - } - - /** - * Information about skip offsets and whether they were applied. - */ - public static class SkipOffsetInfo - { - private final AppliedSkipOffset applied; - private final NotAppliedSkipOffset notApplied; - - @JsonCreator - public SkipOffsetInfo( - @JsonProperty("applied") @Nullable AppliedSkipOffset applied, - @JsonProperty("notApplied") @Nullable NotAppliedSkipOffset notApplied - ) - { - this.applied = applied; - this.notApplied = notApplied; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public AppliedSkipOffset getApplied() - { - return applied; - } - - @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable - public NotAppliedSkipOffset getNotApplied() - { - return notApplied; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - SkipOffsetInfo that = (SkipOffsetInfo) o; - return Objects.equals(applied, that.applied) && - Objects.equals(notApplied, that.notApplied); - } - - @Override - public int hashCode() - { - return Objects.hash(applied, notApplied); - } - } - - /** - * Information about a skip offset that was applied. - */ - public static class AppliedSkipOffset - { - private final String type; - private final Period period; - private final DateTime effectiveEndTime; - - @JsonCreator - public AppliedSkipOffset( - @JsonProperty("type") String type, - @JsonProperty("period") Period period, - @JsonProperty("effectiveEndTime") DateTime effectiveEndTime - ) - { - this.type = type; - this.period = period; - this.effectiveEndTime = effectiveEndTime; - } - - @JsonProperty - public String getType() - { - return type; - } - - @JsonProperty - public Period getPeriod() - { - return period; - } - - @JsonProperty - public DateTime getEffectiveEndTime() - { - return effectiveEndTime; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - AppliedSkipOffset that = (AppliedSkipOffset) o; - return Objects.equals(type, that.type) && - Objects.equals(period, that.period) && - Objects.equals(effectiveEndTime, that.effectiveEndTime); - } - - @Override - public int hashCode() - { - return Objects.hash(type, period, effectiveEndTime); - } - } - - /** - * Information about a skip offset that was not applied. - */ - public static class NotAppliedSkipOffset - { - private final String type; - private final Period period; - private final String reason; - - @JsonCreator - public NotAppliedSkipOffset( - @JsonProperty("type") String type, - @JsonProperty("period") Period period, - @JsonProperty("reason") String reason - ) - { - this.type = type; - this.period = period; - this.reason = reason; - } - - @JsonProperty - public String getType() - { - return type; - } - - @JsonProperty - public Period getPeriod() - { - return period; - } - - @JsonProperty - public String getReason() - { - return reason; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - NotAppliedSkipOffset that = (NotAppliedSkipOffset) o; - return Objects.equals(type, that.type) && - Objects.equals(period, that.period) && - Objects.equals(reason, that.reason); - } - - @Override - public int hashCode() - { - return Objects.hash(type, period, reason); - } - } - - /** - * Represents a search interval and its associated reindexing configuration. - */ - public static class IntervalConfig - { - private final Interval interval; - private final int ruleCount; - private final DataSourceCompactionConfig config; - private final List appliedRules; - - @JsonCreator - public IntervalConfig( - @JsonProperty("interval") Interval interval, - @JsonProperty("ruleCount") int ruleCount, - @JsonProperty("config") DataSourceCompactionConfig config, - @JsonProperty("appliedRules") List appliedRules - ) - { - this.interval = interval; - this.ruleCount = ruleCount; - this.config = config; - this.appliedRules = appliedRules; - } - - @JsonProperty - public Interval getInterval() - { - return interval; - } - - @JsonProperty - public int getRuleCount() - { - return ruleCount; - } - - @JsonProperty - public DataSourceCompactionConfig getConfig() - { - return config; - } - - @JsonProperty - @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") - @JsonSubTypes({ - @JsonSubTypes.Type(value = ReindexingDataSchemaRule.class, name = "dataSchema"), - @JsonSubTypes.Type(value = ReindexingDeletionRule.class, name = "deletion"), - @JsonSubTypes.Type(value = ReindexingSegmentGranularityRule.class, name = "segmentGranularity"), - @JsonSubTypes.Type(value = ReindexingTuningConfigRule.class, name = "tuningConfig"), - @JsonSubTypes.Type(value = ReindexingIOConfigRule.class, name = "ioConfig") - }) - public List getAppliedRules() - { - return appliedRules; - } - - @Override - public boolean equals(Object o) - { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - IntervalConfig that = (IntervalConfig) o; - return ruleCount == that.ruleCount && - Objects.equals(interval, that.interval) && - Objects.equals(config, that.config) && - Objects.equals(appliedRules, that.appliedRules); - } - - @Override - public int hashCode() - { - return Objects.hash(interval, config, ruleCount, appliedRules); - } - } -} diff --git a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx index f613eeaf73b2..44ef05d5d59f 100644 --- a/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx +++ b/web-console/src/dialogs/supervisor-table-action-dialog/supervisor-table-action-dialog.tsx @@ -66,7 +66,7 @@ export const SupervisorTableActionDialog = React.memo(function SupervisorTableAc text: 'History', active: activeTab === 'history', onClick: () => setActiveTab('history'), - } + }, ]; const supervisorEndpointBase = `/druid/indexer/v1/supervisor/${Api.encodePath(supervisorId)}`; From 387eb5a3441c5a244227cf48b643bfae5416b261 Mon Sep 17 00:00:00 2001 From: capistrant Date: Tue, 17 Feb 2026 19:34:00 -0600 Subject: [PATCH 76/90] fix silly jackson serde error --- .../druid/server/compaction/ReindexingDataSchemaRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java index bbf80cc60070..9dfc8619ebfc 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java @@ -45,7 +45,7 @@ public ReindexingDataSchemaRule( @JsonProperty("dimensionsSpec") @Nullable UserCompactionTaskDimensionsConfig dimensionsSpec, @JsonProperty("metricsSpec") @Nullable AggregatorFactory[] metricsSpec, @JsonProperty("queryGranularity") @Nullable Granularity queryGranularity, - @JsonProperty("rolloup") @Nullable Boolean rollup, + @JsonProperty("rollup") @Nullable Boolean rollup, @JsonProperty("projections") @Nullable List projections ) { From 34d31abbd6bc01ca7f2041d9180872f9fdf2bf0d Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 00:10:00 -0600 Subject: [PATCH 77/90] addressing review comments --- .../compact/CompactionSupervisorTest.java | 15 +- .../compact/CascadingReindexingTemplate.java | 23 +- .../CompactionConfigBasedJobTemplate.java | 8 +- ...ranularityTimelineValidationException.java | 2 +- .../CascadingReindexingTemplateTest.java | 722 +++++++++++------- .../transform/CompactionTransformSpec.java | 11 +- .../compaction/IntervalGranularityInfo.java | 32 + .../compaction/ReindexingDataSchemaRule.java | 68 ++ .../compaction/ReindexingDeletionRule.java | 41 + .../compaction/ReindexingIOConfigRule.java | 38 + .../ReindexingSegmentGranularityRule.java | 38 + .../ReindexingTuningConfigRule.java | 38 + 12 files changed, 733 insertions(+), 303 deletions(-) diff --git a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java index c3c160492e08..e09be28eb517 100644 --- a/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java +++ b/embedded-tests/src/test/java/org/apache/druid/testing/embedded/compact/CompactionSupervisorTest.java @@ -20,7 +20,6 @@ package org.apache.druid.testing.embedded.compact; import com.fasterxml.jackson.core.type.TypeReference; -import com.google.common.collect.ImmutableList; import org.apache.druid.catalog.guice.CatalogClientModule; import org.apache.druid.catalog.guice.CatalogCoordinatorModule; import org.apache.druid.common.utils.IdUtils; @@ -350,7 +349,7 @@ public void test_cascadingCompactionTemplate_multiplePeriodsApplyDifferentCompac @Test public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrectly() { - // Virtual Collumns on nested data is only supported with MSQ compaction engine right now. + // Virtual Columns on nested data is only supported with MSQ compaction engine right now. CompactionEngine compactionEngine = CompactionEngine.MSQ; configureCompaction(compactionEngine); @@ -379,13 +378,11 @@ public void test_cascadingReindexing_withVirtualColumnOnNestedData_filtersCorrec Assertions.assertEquals(4, getTotalRowCount()); VirtualColumns virtualColumns = VirtualColumns.create( - ImmutableList.of( - new ExpressionVirtualColumn( - "extractedFieldA", - "json_value(extraInfo, '$.fieldA')", - ColumnType.STRING, - TestExprMacroTable.INSTANCE - ) + new ExpressionVirtualColumn( + "extractedFieldA", + "json_value(extraInfo, '$.fieldA')", + ColumnType.STRING, + TestExprMacroTable.INSTANCE ) ); diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java index 2cab05d5ccb7..16307fa15399 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CascadingReindexingTemplate.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.google.common.annotations.VisibleForTesting; import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.apache.druid.error.InvalidInput; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; import org.apache.druid.java.util.common.DateTimes; @@ -112,19 +113,22 @@ public CascadingReindexingTemplate( @JsonProperty("defaultSegmentGranularity") Granularity defaultSegmentGranularity ) { - this.dataSource = Objects.requireNonNull(dataSource, "'dataSource' cannot be null"); - this.ruleProvider = Objects.requireNonNull(ruleProvider, "'ruleProvider' cannot be null"); + InvalidInput.conditionalException(dataSource != null, "'dataSource' cannot be null"); + this.dataSource = dataSource; + + InvalidInput.conditionalException(ruleProvider != null, "'ruleProvider' cannot be null"); + this.ruleProvider = ruleProvider; + this.engine = engine; this.taskContext = taskContext; this.taskPriority = Objects.requireNonNullElse(taskPriority, DEFAULT_COMPACTION_TASK_PRIORITY); this.inputSegmentSizeBytes = Objects.requireNonNullElse(inputSegmentSizeBytes, DEFAULT_INPUT_SEGMENT_SIZE_BYTES); - this.defaultSegmentGranularity = Objects.requireNonNull( - defaultSegmentGranularity, - "'defaultSegmentGranularity' cannot be null" - ); + + InvalidInput.conditionalException(defaultSegmentGranularity != null, "'defaultSegmentGranularity' cannot be null"); + this.defaultSegmentGranularity = defaultSegmentGranularity; if (skipOffsetFromNow != null && skipOffsetFromLatest != null) { - throw new IAE("Cannot set both skipOffsetFromNow and skipOffsetFromLatest"); + throw InvalidInput.exception("Cannot set both skipOffsetFromNow and skipOffsetFromLatest"); } this.skipOffsetFromNow = skipOffsetFromNow; this.skipOffsetFromLatest = skipOffsetFromLatest; @@ -524,9 +528,8 @@ private List generateBaseSegmentGranularityAlignedTimel private List createDefaultGranularityTimeline(List nonSegmentGranThresholds) { if (nonSegmentGranThresholds.isEmpty()) { - throw new IAE( - "CascadingReindexingTemplate requires at least one reindexing rule " - + "(segment granularity or other type)" + throw InvalidInput.exception( + "CascadingReindexingTemplate requires at least one reindexing rule (segment granularity or other type)" ); } diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java index da0a4dd11d64..25643231b2c8 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/CompactionConfigBasedJobTemplate.java @@ -48,7 +48,7 @@ public class CompactionConfigBasedJobTemplate implements CompactionJobTemplate { private final DataSourceCompactionConfig config; - private final ReindexingConfigOptimizer configFinalizer; + private final ReindexingConfigOptimizer configOptimizer; public CompactionConfigBasedJobTemplate(DataSourceCompactionConfig config) { @@ -57,11 +57,11 @@ public CompactionConfigBasedJobTemplate(DataSourceCompactionConfig config) public CompactionConfigBasedJobTemplate( DataSourceCompactionConfig config, - ReindexingConfigOptimizer configFinalizer + ReindexingConfigOptimizer configOptimizer ) { this.config = config; - this.configFinalizer = configFinalizer; + this.configOptimizer = configOptimizer; } @Nullable @@ -93,7 +93,7 @@ public List createCompactionJobs( final CompactionCandidate candidate = segmentIterator.next(); // Allow template-specific customization of the config per candidate - DataSourceCompactionConfig finalConfig = configFinalizer.optimizeConfig(config, candidate, params); + DataSourceCompactionConfig finalConfig = configOptimizer.optimizeConfig(config, candidate, params); ClientCompactionTaskQuery taskPayload = CompactSegments.createCompactionTask( candidate, diff --git a/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java b/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java index 7246af135c97..bfb38295d1c0 100644 --- a/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java +++ b/indexing-service/src/main/java/org/apache/druid/indexing/compact/SegmentGranularityTimelineValidationException.java @@ -46,7 +46,7 @@ public SegmentGranularityTimelineValidationException( "Invalid segment granularity timeline for dataSource[%s]: " + "Interval[%s] with granularity[%s] is more recent than " + "interval[%s] with granularity[%s], but has a coarser granularity. " - + "Segment granularity must stay the same or become finer as data ages from present to past.", + + "Segment granularity must stay the same or become coarser as data ages from present to past.", dataSource, newerInterval, newerGranularity, diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 8a805a5a9212..40b44d77acab 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; +import org.apache.druid.error.DruidException; import org.apache.druid.guice.SupervisorModule; import org.apache.druid.indexer.CompactionEngine; import org.apache.druid.indexing.input.DruidInputSource; @@ -177,8 +178,8 @@ public void test_constructor_setBothSkipOffsetStrategiesThrowsException() final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.replay(mockProvider); - IllegalArgumentException exception = Assert.assertThrows( - IllegalArgumentException.class, + DruidException exception = Assert.assertThrows( + DruidException.class, () -> new CascadingReindexingTemplate( "testDataSource", null, @@ -196,6 +197,77 @@ public void test_constructor_setBothSkipOffsetStrategiesThrowsException() EasyMock.verify(mockProvider); } + @Test + public void test_constructor_nullDataSourceThrowsException() + { + final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.replay(mockProvider); + + DruidException exception = Assert.assertThrows( + DruidException.class, + () -> new CascadingReindexingTemplate( + null, // null dataSource + null, + null, + mockProvider, + null, + null, + null, + null, + Granularities.DAY + ) + ); + + Assert.assertTrue(exception.getMessage().contains("'dataSource' cannot be null")); + EasyMock.verify(mockProvider); + } + + @Test + public void test_constructor_nullRuleProviderThrowsException() + { + DruidException exception = Assert.assertThrows( + DruidException.class, + () -> new CascadingReindexingTemplate( + "testDataSource", + null, + null, + null, // null ruleProvider + null, + null, + null, + null, + Granularities.DAY + ) + ); + + Assert.assertTrue(exception.getMessage().contains("'ruleProvider' cannot be null")); + } + + @Test + public void test_constructor_nullDefaultSegmentGranularityThrowsException() + { + final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); + EasyMock.replay(mockProvider); + + DruidException exception = Assert.assertThrows( + DruidException.class, + () -> new CascadingReindexingTemplate( + "testDataSource", + null, + null, + mockProvider, + null, + null, + null, + null, + null // null defaultSegmentGranularity + ) + ); + + Assert.assertTrue(exception.getMessage().contains("'defaultSegmentGranularity' cannot be null")); + EasyMock.verify(mockProvider); + } + @Test public void test_createCompactionJobs_simple() { @@ -393,12 +465,12 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() { DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + ReindexingSegmentGranularityRule hourRule = new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), - new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY), - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH) - )) + .segmentGranularityRules(List.of(hourRule, dayRule, monthRule)) .build(); CascadingReindexingTemplate template = new CascadingReindexingTemplate( @@ -413,18 +485,26 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(3, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getInterval().getEnd()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-10-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-10-01T00:00:00Z"), DateTimes.of("2024-12-29T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-29T00:00:00Z"), DateTimes.of("2025-01-22T16:00:00Z")), + Granularities.HOUR, + hourRule + ) + ); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(2).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -469,17 +549,18 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl { DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + ReindexingSegmentGranularityRule hourRule = new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH); + + // The data schema rules are here to trigger splits in the base timeline for granularity rules. ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(7), Granularities.HOUR), - new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY), - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH) - )) + .segmentGranularityRules(List.of(hourRule, dayRule, monthRule)) .dataSchemaRules(List.of( - new ReindexingDataSchemaRule("metrics-8d", null, Period.days(8), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-45d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-100d", null, Period.days(100), null, new AggregatorFactory[0], null, null, null) + createReindexingDataSchemaRule("metrics-8d", Period.days(8)), + createReindexingDataSchemaRule("metrics-14d", Period.days(14)), + createReindexingDataSchemaRule("metrics-45d", Period.days(45)), + createReindexingDataSchemaRule("metrics-100d", Period.days(100)) )) .build(); @@ -495,30 +576,46 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(7, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-10-21T00:00:00Z"), intervals.get(2).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(2).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(3).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(3).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(4).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(5).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(5).getInterval().getEnd()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-10-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-10-01T00:00:00Z"), DateTimes.of("2024-10-21T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-10-21T00:00:00Z"), DateTimes.of("2024-12-15T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-15T00:00:00Z"), DateTimes.of("2024-12-29T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-29T00:00:00Z"), DateTimes.of("2025-01-15T16:00:00Z")), + Granularities.HOUR, + hourRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2025-01-15T16:00:00Z"), DateTimes.of("2025-01-21T16:00:00Z")), + Granularities.HOUR, + hourRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2025-01-21T16:00:00Z"), DateTimes.of("2025-01-22T16:00:00Z")), + Granularities.HOUR, + hourRule + ) + ); - Assert.assertEquals(DateTimes.of("2025-01-21T16:00:00Z"), intervals.get(6).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(6).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -561,8 +658,9 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() .dataSchemaRules(List.of( new ReindexingDataSchemaRule("metrics-8d", null, Period.days(8), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-45d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null) + createReindexingDataSchemaRule("metrics-8d", Period.days(8)), + createReindexingDataSchemaRule("metrics-14d", Period.days(14)), + createReindexingDataSchemaRule("metrics-45d", Period.days(45)) )) .build(); @@ -578,18 +676,27 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(3, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(1).getInterval().getEnd()); + // When no segment granularity rules exist, a synthetic rule is created with the smallest period + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-12-15T00:00:00Z")), + Granularities.DAY, + null // Synthetic rule has no source + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-15T00:00:00Z"), DateTimes.of("2025-01-15T00:00:00Z")), + Granularities.DAY, + null // Synthetic rule has no source + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2025-01-15T00:00:00Z"), DateTimes.of("2025-01-21T00:00:00Z")), + Granularities.DAY, + null // Synthetic rule has no source + ) + ); - Assert.assertEquals(DateTimes.of("2025-01-15T00:00:00Z"), intervals.get(2).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-21T00:00:00Z"), intervals.get(2).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -637,11 +744,11 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen { DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(3), Granularities.MONTH), - new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY) - )) + .segmentGranularityRules(List.of(monthRule, dayRule)) .dataSchemaRules(List.of( new ReindexingDataSchemaRule("metrics-7d", null, Period.days(7), null, new AggregatorFactory[0], null, null, null), new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), @@ -661,24 +768,36 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen Granularities.HOUR ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(5, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-10-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-12-29T00:00:00Z"), intervals.get(2).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(2).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2025-01-08T16:00:00Z"), intervals.get(3).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(3).getInterval().getEnd()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-10-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-10-01T00:00:00Z"), DateTimes.of("2024-12-29T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-29T00:00:00Z"), DateTimes.of("2025-01-08T16:00:00Z")), + Granularities.HOUR, + null // Synthetic prepended rule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2025-01-08T16:00:00Z"), DateTimes.of("2025-01-15T16:00:00Z")), + Granularities.HOUR, + null // Synthetic prepended rule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2025-01-15T16:00:00Z"), DateTimes.of("2025-01-22T16:00:00Z")), + Granularities.HOUR, + null // Synthetic prepended rule + ) + ); - Assert.assertEquals(DateTimes.of("2025-01-15T16:00:00Z"), intervals.get(4).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-22T16:00:00Z"), intervals.get(4).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -728,18 +847,20 @@ public void test_generateAlignedSearchIntervals() { DateTime referenceTime = DateTimes.of("2024-02-04T22:12:04.873Z"); - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("month-rule", null, Period.years(1), Granularities.YEAR), - new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.MONTH), - new ReindexingSegmentGranularityRule("day-rule", null, Period.days(7), Granularities.DAY) - )) - .dataSchemaRules(List.of( - new ReindexingDataSchemaRule("metrics-7d", null, Period.days(1), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), - new ReindexingDataSchemaRule("metrics-21d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null) - )) - .build(); + ReindexingSegmentGranularityRule yearRule = new ReindexingSegmentGranularityRule("year-rule", null, Period.years(1), Granularities.YEAR); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.days(7), Granularities.DAY); + + ReindexingRuleProvider provider = + InlineReindexingRuleProvider + .builder() + .segmentGranularityRules(List.of(yearRule, monthRule, dayRule)) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-1d", null, Period.days(1), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-14d", null, Period.days(14), null, new AggregatorFactory[0], null, null, null), + new ReindexingDataSchemaRule("metrics-45d", null, Period.days(45), null, new AggregatorFactory[0], null, null, null) + )) + .build(); CascadingReindexingTemplate template = new CascadingReindexingTemplate( "testDS", @@ -753,27 +874,41 @@ public void test_generateAlignedSearchIntervals() Granularities.HOUR ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(6, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2023-01-01T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(1).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2023-12-01T00:00:00Z"), intervals.get(2).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(2).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-01-01T00:00:00Z"), intervals.get(3).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(3).getInterval().getEnd()); - - Assert.assertEquals(DateTimes.of("2024-01-21T00:00:00Z"), intervals.get(4).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00Z"), intervals.get(4).getInterval().getEnd()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2023-01-01T00:00:00Z")), + Granularities.YEAR, + yearRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2023-01-01T00:00:00Z"), DateTimes.of("2023-12-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2023-12-01T00:00:00Z"), DateTimes.of("2024-01-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-01-01T00:00:00Z"), DateTimes.of("2024-01-21T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-01-21T00:00:00Z"), DateTimes.of("2024-01-28T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-01-28T00:00:00"), DateTimes.of("2024-02-03T22:00:00")), + Granularities.HOUR, + null // Synthetic prepended rule + ) + ); - Assert.assertEquals(DateTimes.of("2024-01-28T00:00:00"), intervals.get(5).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-02-03T22:00:00"), intervals.get(5).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -809,8 +944,8 @@ public void test_generateAlignedSearchIntervals_noRulesThrowsException() Granularities.DAY ); - IllegalArgumentException exception = Assert.assertThrows( - IllegalArgumentException.class, + DruidException exception = Assert.assertThrows( + DruidException.class, () -> template.generateAlignedSearchIntervals(referenceTime) ); @@ -853,10 +988,10 @@ public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundar { DateTime referenceTime = DateTimes.of("2025-02-01T00:00:00Z"); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH) - )) + .segmentGranularityRules(List.of(monthRule)) .dataSchemaRules(List.of( new ReindexingDataSchemaRule("metrics-1m", null, Period.months(1), null, new AggregatorFactory[0], null, null, null) )) @@ -874,12 +1009,16 @@ public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundar Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(1, intervals.size()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2025-01-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ) + ); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2025-01-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -916,10 +1055,10 @@ public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTim { DateTime referenceTime = DateTimes.of("2025-01-01T01:00:00Z"); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.days(1), Granularities.DAY); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("day-rule", null, Period.days(1), Granularities.DAY) - )) + .segmentGranularityRules(List.of(dayRule)) .dataSchemaRules(List.of( new ReindexingDataSchemaRule("metrics-12h", null, Period.hours(12), null, new AggregatorFactory[0], null, null, null) )) @@ -937,12 +1076,16 @@ public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTim Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(1, intervals.size()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-12-31T00:00:00Z")), + Granularities.DAY, + dayRule + ) + ); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-31T00:00:00Z"), intervals.get(0).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -982,10 +1125,10 @@ public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() { DateTime referenceTime = DateTimes.of("2025-01-15T00:00:00Z"); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule("day-rule", null, Period.months(1), Granularities.DAY); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.DAY) - )) + .segmentGranularityRules(List.of(dayRule)) .dataSchemaRules(List.of( new ReindexingDataSchemaRule("metrics-33d-6h", null, Period.hours(33 * 24 + 6), null, new AggregatorFactory[0], null, null, null), new ReindexingDataSchemaRule("metrics-33d-18h", null, Period.hours(33 * 24 + 18), null, new AggregatorFactory[0], null, null, null) @@ -1004,15 +1147,21 @@ public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); - - Assert.assertEquals(2, intervals.size()); - - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(0).getInterval().getEnd()); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-12-12T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-12T00:00:00Z"), DateTimes.of("2024-12-15T00:00:00Z")), + Granularities.DAY, + dayRule + ) + ); - Assert.assertEquals(DateTimes.of("2024-12-12T00:00:00Z"), intervals.get(1).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-15T00:00:00Z"), intervals.get(1).getInterval().getEnd()); + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); } /** @@ -1044,10 +1193,10 @@ public void test_generateAlignedSearchIntervals_singleRuleOnly() { DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH); + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("month-rule", null, Period.months(1), Granularities.MONTH) - )) + .segmentGranularityRules(List.of(monthRule)) .build(); CascadingReindexingTemplate template = new CascadingReindexingTemplate( @@ -1062,12 +1211,141 @@ public void test_generateAlignedSearchIntervals_singleRuleOnly() Granularities.DAY ); - List intervals = template.generateAlignedSearchIntervals(referenceTime); + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-12-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ) + ); + + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); + } + + /** + * TEST: Validation failure - default granularity is coarser than most recent segment granularity rule + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P30D→HOUR, P90D→DAY
    • + *
    • Other Rules: P7D-metrics (finer than P30D, triggers prepending with default granularity)
    • + *
    • Default Segment Granularity: MONTH (COARSER than HOUR!)
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Sort rules by period: P90D→DAY (oldest), P30D→HOUR (newest)
    2. + *
    3. P7D metrics is finer than P30D, so prepend interval with default MONTH granularity
    4. + *
    5. Timeline would be: [-∞, DAY_boundary) DAY, [DAY_boundary, HOUR_boundary) HOUR, [HOUR_boundary, MONTH_boundary) MONTH
    6. + *
    7. Validation: HOUR → MONTH progression means granularity is getting COARSER toward present
    8. + *
    + *

    + * EXPECTED: IllegalArgumentException with message about invalid granularity timeline + */ + @Test + public void test_generateAlignedSearchIntervals_failsWhenDefaultGranularityIsCoarserThanMostRecentSegmentGranRule() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = + InlineReindexingRuleProvider + .builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) + )) + .dataSchemaRules(List.of( + new ReindexingDataSchemaRule("metrics-7d", null, Period.days(7), null, new AggregatorFactory[0], null, null, null) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.MONTH // MONTH is coarser than HOUR! + ); + + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> template.generateAlignedSearchIntervals(referenceTime) + ); + + Assert.assertTrue( + exception.getMessage().contains("Invalid segment granularity timeline") + ); + Assert.assertTrue( + exception.getMessage().contains("coarser granularity") + ); + } + + /** + * TEST: Validation failure - older rule has finer granularity than newer rule + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P30D→DAY, P90D→HOUR
    • + *
    • Other Rules: None
    • + *
    • Default Segment Granularity: DAY
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Sort rules by period: P90D→HOUR (oldest), P30D→DAY (newest)
    2. + *
    3. Timeline would be: [-∞, HOUR_boundary) HOUR, [HOUR_boundary, DAY_boundary) DAY
    4. + *
    5. Validation: HOUR → DAY progression means granularity is getting COARSER toward present
    6. + *
    7. This violates the constraint: older data (P90D) has HOUR granularity, newer data (P30D) has DAY granularity
    8. + *
    + *

    + * EXPECTED: IllegalArgumentException with message about invalid granularity timeline + */ + @Test + public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranularityThanNewerRule() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingRuleProvider provider = + InlineReindexingRuleProvider + .builder() + .segmentGranularityRules(List.of( + new ReindexingSegmentGranularityRule("day-rule", null, Period.days(30), Granularities.DAY), + new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(90), Granularities.HOUR) + )) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); - Assert.assertEquals(1, intervals.size()); + IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException.class, + () -> template.generateAlignedSearchIntervals(referenceTime) + ); - Assert.assertEquals(DateTimes.MIN, intervals.get(0).getInterval().getStart()); - Assert.assertEquals(DateTimes.of("2024-12-01T00:00:00Z"), intervals.get(0).getInterval().getEnd()); + Assert.assertTrue( + exception.getMessage().contains("Invalid segment granularity timeline") + ); + Assert.assertTrue( + exception.getMessage().contains("coarser granularity") + ); } private static class TestCascadingReindexingTemplate extends CascadingReindexingTemplate @@ -1177,127 +1455,6 @@ private CompactionJobParams createMockParams(DateTime referenceTime, SegmentTime return mockParams; } - /** - * TEST: Validation failure - default granularity is coarser than most recent segment granularity rule - *

    - * REFERENCE TIME: 2025-01-29T16:15:00Z - *

    - * INPUT RULES: - *

      - *
    • Segment Granularity Rules: P30D→HOUR, P90D→DAY
    • - *
    • Other Rules: P7D-metrics (finer than P30D, triggers prepending with default granularity)
    • - *
    • Default Segment Granularity: MONTH (COARSER than HOUR!)
    • - *
    - *

    - * PROCESSING: - *

      - *
    1. Sort rules by period: P90D→DAY (oldest), P30D→HOUR (newest)
    2. - *
    3. P7D metrics is finer than P30D, so prepend interval with default MONTH granularity
    4. - *
    5. Timeline would be: [-∞, DAY_boundary) DAY, [DAY_boundary, HOUR_boundary) HOUR, [HOUR_boundary, MONTH_boundary) MONTH
    6. - *
    7. Validation: HOUR → MONTH progression means granularity is getting COARSER toward present
    8. - *
    - *

    - * EXPECTED: IllegalArgumentException with message about invalid granularity timeline - */ - @Test - public void test_generateAlignedSearchIntervals_failsWhenDefaultGranularityIsCoarserThanMostRecentSegmentGranRule() - { - DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(30), Granularities.HOUR), - new ReindexingSegmentGranularityRule("day-rule", null, Period.days(90), Granularities.DAY) - )) - .dataSchemaRules(List.of( - new ReindexingDataSchemaRule("metrics-7d", null, Period.days(7), null, new AggregatorFactory[0], null, null, null) - )) - .build(); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - provider, - null, - null, - null, - null, - Granularities.MONTH // MONTH is coarser than HOUR! - ); - - IllegalArgumentException exception = Assert.assertThrows( - IllegalArgumentException.class, - () -> template.generateAlignedSearchIntervals(referenceTime) - ); - - Assert.assertTrue( - exception.getMessage().contains("Invalid segment granularity timeline") - ); - Assert.assertTrue( - exception.getMessage().contains("coarser granularity") - ); - } - - /** - * TEST: Validation failure - older rule has finer granularity than newer rule - *

    - * REFERENCE TIME: 2025-01-29T16:15:00Z - *

    - * INPUT RULES: - *

      - *
    • Segment Granularity Rules: P30D→DAY, P90D→HOUR
    • - *
    • Other Rules: None
    • - *
    • Default Segment Granularity: DAY
    • - *
    - *

    - * PROCESSING: - *

      - *
    1. Sort rules by period: P90D→HOUR (oldest), P30D→DAY (newest)
    2. - *
    3. Timeline would be: [-∞, HOUR_boundary) HOUR, [HOUR_boundary, DAY_boundary) DAY
    4. - *
    5. Validation: HOUR → DAY progression means granularity is getting COARSER toward present
    6. - *
    7. This violates the constraint: older data (P90D) has HOUR granularity, newer data (P30D) has DAY granularity
    8. - *
    - *

    - * EXPECTED: IllegalArgumentException with message about invalid granularity timeline - */ - @Test - public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranularityThanNewerRule() - { - DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); - - ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() - .segmentGranularityRules(List.of( - new ReindexingSegmentGranularityRule("day-rule", null, Period.days(30), Granularities.DAY), - new ReindexingSegmentGranularityRule("hour-rule", null, Period.days(90), Granularities.HOUR) - )) - .build(); - - CascadingReindexingTemplate template = new CascadingReindexingTemplate( - "testDS", - null, - null, - provider, - null, - null, - null, - null, - Granularities.DAY - ); - - IllegalArgumentException exception = Assert.assertThrows( - IllegalArgumentException.class, - () -> template.generateAlignedSearchIntervals(referenceTime) - ); - - Assert.assertTrue( - exception.getMessage().contains("Invalid segment granularity timeline") - ); - Assert.assertTrue( - exception.getMessage().contains("coarser granularity") - ); - } - private DruidInputSource createMockSource() { final Interval[] capturedInterval = new Interval[1]; @@ -1315,4 +1472,23 @@ private DruidInputSource createMockSource() EasyMock.replay(mockSource); return mockSource; } + + /** + * Helper method to create a ReindexingDataSchemaRule with minimal required fields for testing + *

    + * Helps quickly generate multiple rules to be used in testing formation of timelines and splits. + */ + private ReindexingDataSchemaRule createReindexingDataSchemaRule(String name, Period period) + { + return new ReindexingDataSchemaRule( + name, + null, + period, + null, + new AggregatorFactory[0], + null, + null, + null + ); + } } diff --git a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java index 1040315ed89f..0a0c2243875d 100644 --- a/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java +++ b/processing/src/main/java/org/apache/druid/segment/transform/CompactionTransformSpec.java @@ -48,20 +48,20 @@ public static CompactionTransformSpec of(@Nullable TransformSpec transformSpec) return null; } - return new CompactionTransformSpec(transformSpec.getFilter(), null); + return new CompactionTransformSpec(transformSpec.getFilter(), VirtualColumns.EMPTY); } @Nullable private final DimFilter filter; - @Nullable private final VirtualColumns virtualColumns; + private final VirtualColumns virtualColumns; @JsonCreator public CompactionTransformSpec( @JsonProperty("filter") final DimFilter filter, - @JsonProperty("virtualColumns") final VirtualColumns virtualColumns + @JsonProperty("virtualColumns") @Nullable final VirtualColumns virtualColumns ) { this.filter = filter; - this.virtualColumns = virtualColumns; + this.virtualColumns = virtualColumns == null ? VirtualColumns.EMPTY : virtualColumns; } @JsonProperty @@ -72,8 +72,7 @@ public DimFilter getFilter() } @JsonProperty - @JsonInclude(JsonInclude.Include.NON_NULL) - @Nullable + @JsonInclude(JsonInclude.Include.NON_EMPTY) public VirtualColumns getVirtualColumns() { return virtualColumns; diff --git a/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java b/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java index 2f9df47ce77c..691010a4769e 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java +++ b/server/src/main/java/org/apache/druid/server/compaction/IntervalGranularityInfo.java @@ -23,6 +23,7 @@ import org.joda.time.Interval; import javax.annotation.Nullable; +import java.util.Objects; /** * Associates a time interval with its segment granularity and optional source rule. @@ -60,4 +61,35 @@ public ReindexingSegmentGranularityRule getSourceRule() { return sourceRule; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IntervalGranularityInfo that = (IntervalGranularityInfo) o; + return Objects.equals(interval, that.interval) + && Objects.equals(granularity, that.granularity) + && Objects.equals(sourceRule, that.sourceRule); + } + + @Override + public int hashCode() + { + return Objects.hash(interval, granularity, sourceRule); + } + + @Override + public String toString() + { + return "IntervalGranularityInfo{" + + "interval=" + interval + + ", granularity=" + granularity + + ", sourceRule=" + (sourceRule != null ? sourceRule.getId() : "null") + + '}'; + } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java index 9dfc8619ebfc..7c5c25c78ed6 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java @@ -28,8 +28,26 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.Arrays; import java.util.List; +import java.util.Objects; +/** + * A {@link ReindexingRule} that specifies a data schema for reindexing tasks to configure. + *

    + * This rule allows users to specify dimensionsspec metricsspec, query granularity, rollup, and projections for reindexing tasks to apply + *

    + * This is a non-additive rule. Multiple data schema rules cannot be applied to the same interval being reindexed. + *

    + * Example inline usage: + *

    {@code
    + * {
    + *   "id": "change-query-granularity-30d",
    + *   "olderThan": "P30D",
    + *   "queryGranularity": "HOUR",
    + *   "rollup": true
    + * }
    + */ public class ReindexingDataSchemaRule extends AbstractReindexingRule { private final UserCompactionTaskDimensionsConfig dimensionsSpec; @@ -86,4 +104,54 @@ public Boolean getRollup() { return rollup; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReindexingDataSchemaRule that = (ReindexingDataSchemaRule) o; + return Objects.equals(getId(), that.getId()) + && Objects.equals(getDescription(), that.getDescription()) + && Objects.equals(getOlderThan(), that.getOlderThan()) + && Objects.equals(dimensionsSpec, that.dimensionsSpec) + && Objects.deepEquals(metricsSpec, that.metricsSpec) + && Objects.equals(queryGranularity, that.queryGranularity) + && Objects.equals(rollup, that.rollup) + && Objects.equals(projections, that.projections); + } + + @Override + public int hashCode() + { + return Objects.hash( + getId(), + getDescription(), + getOlderThan(), + dimensionsSpec, + Objects.hashCode(metricsSpec), + queryGranularity, + rollup, + projections + ); + } + + @Override + public String toString() + { + return "ReindexingDataSchemaRule{" + + "id='" + getId() + '\'' + + ", description='" + getDescription() + '\'' + + ", olderThan=" + getOlderThan() + + ", dimensionsSpec=" + dimensionsSpec + + ", metricsSpec=" + Arrays.toString(metricsSpec) + + ", queryGranularity=" + queryGranularity + + ", rollup=" + rollup + + ", projections=" + projections + + '}'; + } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java index af471b4b4c7e..43465cc8bc52 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java @@ -113,4 +113,45 @@ public VirtualColumns getVirtualColumns() { return virtualColumns; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReindexingDeletionRule that = (ReindexingDeletionRule) o; + return Objects.equals(getId(), that.getId()) + && Objects.equals(getDescription(), that.getDescription()) + && Objects.equals(getOlderThan(), that.getOlderThan()) + && Objects.equals(deleteWhere, that.deleteWhere) + && Objects.equals(virtualColumns, that.virtualColumns); + } + + @Override + public int hashCode() + { + return Objects.hash( + getId(), + getDescription(), + getOlderThan(), + deleteWhere, + virtualColumns + ); + } + + @Override + public String toString() + { + return "ReindexingDeletionRule{" + + "id='" + getId() + '\'' + + ", description='" + getDescription() + '\'' + + ", olderThan=" + getOlderThan() + + ", deleteWhere=" + deleteWhere + + ", virtualColumns=" + virtualColumns + + '}'; + } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java index 38b65b263723..25f4af46ef20 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java @@ -66,4 +66,42 @@ public UserCompactionTaskIOConfig getIoConfig() { return ioConfig; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReindexingIOConfigRule that = (ReindexingIOConfigRule) o; + return Objects.equals(getId(), that.getId()) + && Objects.equals(getDescription(), that.getDescription()) + && Objects.equals(getOlderThan(), that.getOlderThan()) + && Objects.equals(ioConfig, that.ioConfig); + } + + @Override + public int hashCode() + { + return Objects.hash( + getId(), + getDescription(), + getOlderThan(), + ioConfig + ); + } + + @Override + public String toString() + { + return "ReindexingIOConfigRule{" + + "id='" + getId() + '\'' + + ", description='" + getDescription() + '\'' + + ", olderThan=" + getOlderThan() + + ", ioConfig=" + ioConfig + + '}'; + } } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java index b2f8c6480d42..0393bdfcd054 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java @@ -68,4 +68,42 @@ public Granularity getSegmentGranularity() return segmentGranularity; } + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReindexingSegmentGranularityRule that = (ReindexingSegmentGranularityRule) o; + return Objects.equals(getId(), that.getId()) + && Objects.equals(getDescription(), that.getDescription()) + && Objects.equals(getOlderThan(), that.getOlderThan()) + && Objects.equals(segmentGranularity, that.segmentGranularity); + } + + @Override + public int hashCode() + { + return Objects.hash( + getId(), + getDescription(), + getOlderThan(), + segmentGranularity + ); + } + + @Override + public String toString() + { + return "ReindexingSegmentGranularityRule{" + + "id='" + getId() + '\'' + + ", description='" + getDescription() + '\'' + + ", olderThan=" + getOlderThan() + + ", segmentGranularity=" + segmentGranularity + + '}'; + } + } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java index b27123ef4f2b..427643f768f7 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java @@ -75,4 +75,42 @@ public UserCompactionTaskQueryTuningConfig getTuningConfig() { return tuningConfig; } + + @Override + public boolean equals(Object o) + { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ReindexingTuningConfigRule that = (ReindexingTuningConfigRule) o; + return Objects.equals(getId(), that.getId()) + && Objects.equals(getDescription(), that.getDescription()) + && Objects.equals(getOlderThan(), that.getOlderThan()) + && Objects.equals(tuningConfig, that.tuningConfig); + } + + @Override + public int hashCode() + { + return Objects.hash( + getId(), + getDescription(), + getOlderThan(), + tuningConfig + ); + } + + @Override + public String toString() + { + return "ReindexingTuningConfigRule{" + + "id='" + getId() + '\'' + + ", description='" + getDescription() + '\'' + + ", olderThan=" + getOlderThan() + + ", tuningConfig=" + tuningConfig + + '}'; + } } From 30d2d773e967588277f2c62c1a1e1e0c5053d38f Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 00:22:35 -0600 Subject: [PATCH 78/90] Create strict allow list for segment granularity options --- .../ReindexingSegmentGranularityRule.java | 23 ++++++++- .../ReindexingSegmentGranularityRuleTest.java | 51 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java index 0393bdfcd054..47bcc2b59116 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRule.java @@ -21,18 +21,22 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.error.InvalidInput; +import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; import org.joda.time.Period; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.util.List; import java.util.Objects; /** * A {@link ReindexingRule} that specifies a segment granularity for reindexing tasks to configure. *

    * This rule controls how time-series data is bucketed into segments during reindexing. For example, changing from - * 15-minute segments to hourly segments reduces segment count. + * 15-minute segments to hourly segments reduces segment count. There is a strict allow list of supported granularities + * to prevent misconfiguration. *

    * This is a non-additive rule. Multiple segment granularity rules cannot be applied to the same segment. *

    @@ -48,6 +52,16 @@ */ public class ReindexingSegmentGranularityRule extends AbstractReindexingRule { + private static final List SUPPORTED_SEGMENT_GRANULARITIES = List.of( + Granularities.MINUTE, + Granularities.FIFTEEN_MINUTE, + Granularities.HOUR, + Granularities.DAY, + Granularities.MONTH, + Granularities.QUARTER, + Granularities.YEAR + ); + private final Granularity segmentGranularity; @JsonCreator @@ -59,7 +73,12 @@ public ReindexingSegmentGranularityRule( ) { super(id, description, olderThan); - this.segmentGranularity = Objects.requireNonNull(segmentGranularity); + InvalidInput.conditionalException( + SUPPORTED_SEGMENT_GRANULARITIES.contains(segmentGranularity), + "Unsupported segment granularity [%s]. Supported values are: MINUTE, FIFTEEN_MINUTE, HOUR, DAY, MONTH, QUARTER, YEAR", + segmentGranularity + ); + this.segmentGranularity = segmentGranularity; } @JsonProperty diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java index 041e90f45ac2..9aaf184afe1e 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java @@ -19,11 +19,14 @@ package org.apache.druid.server.compaction; +import org.apache.druid.error.DruidException; 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.granularity.Granularity; +import org.apache.druid.java.util.common.granularity.PeriodGranularity; import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.joda.time.Period; import org.junit.Assert; @@ -162,4 +165,52 @@ public void test_constructor_nullGranularity_throwsNullPointerException() () -> new ReindexingSegmentGranularityRule("test-id", "description", PERIOD_7_DAYS, null) ); } + + @Test + public void test_constructor_supportedGranularities_allSucceed() + { + Granularity[] supportedGranularities = { + Granularities.MINUTE, + Granularities.FIFTEEN_MINUTE, + Granularities.HOUR, + Granularities.DAY, + Granularities.MONTH, + Granularities.QUARTER, + Granularities.YEAR + }; + + for (Granularity granularity : supportedGranularities) { + ReindexingSegmentGranularityRule rule = new ReindexingSegmentGranularityRule( + "test-id", + "description", + PERIOD_7_DAYS, + granularity + ); + Assert.assertEquals(granularity, rule.getSegmentGranularity()); + } + } + + @Test + public void test_constructor_unsupportedGranularities_allThrowDruidException() + { + Granularity[] unsupportedGranularities = { + Granularities.THIRTY_MINUTE, + Granularities.SIX_HOUR, + Granularities.EIGHT_HOUR, + Granularities.WEEK, + new PeriodGranularity(Period.days(3), null, DateTimeZone.UTC), // Custom period + new PeriodGranularity(Period.days(1), null, DateTimeZone.forID("America/Los_Angeles")) // With timezone + }; + + for (Granularity granularity : unsupportedGranularities) { + DruidException exception = Assert.assertThrows( + DruidException.class, + () -> new ReindexingSegmentGranularityRule("test-id", "description", PERIOD_7_DAYS, granularity) + ); + Assert.assertTrue( + "Expected exception message to contain 'Unsupported segment granularity' but got: " + exception.getMessage(), + exception.getMessage().contains("Unsupported segment granularity") + ); + } + } } From a38117954090f2fb27b635a4c9044e959fec479b Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 00:32:09 -0600 Subject: [PATCH 79/90] add some compaction transform serde tests out of paranoia --- .../CompactionTransformSpecTest.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java b/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java index e513e52cfeb9..110605286bd1 100644 --- a/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java +++ b/processing/src/test/java/org/apache/druid/segment/transform/CompactionTransformSpecTest.java @@ -73,4 +73,63 @@ public void testSerde() throws IOException ); Assert.assertEquals(expected, fromJson); } + + @Test + public void testSerde_emptyVirtualColumns_notSerializedForFingerprintConsistency() throws IOException + { + final ObjectMapper mapper = new DefaultObjectMapper(); + + // Spec with VirtualColumns.EMPTY (new code path) + final CompactionTransformSpec withEmpty = new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + VirtualColumns.EMPTY + ); + + // Spec with null (old code path before VirtualColumns support) + final CompactionTransformSpec withNull = new CompactionTransformSpec( + new SelectorDimFilter("dim1", "foo", null), + null + ); + + // Serialize both + final String jsonWithEmpty = mapper.writeValueAsString(withEmpty); + final String jsonWithNull = mapper.writeValueAsString(withNull); + + // Both should produce identical JSON (no virtualColumns field) + // This ensures fingerprint consistency: old segments (no VC) match new segments (EMPTY VC) + Assert.assertEquals( + "VirtualColumns.EMPTY should serialize identically to null for fingerprint consistency", + jsonWithNull, + jsonWithEmpty + ); + + // Verify virtualColumns field is not present in either JSON + Assert.assertFalse( + "virtualColumns field should not appear in JSON when empty", + jsonWithEmpty.contains("virtualColumns") + ); + Assert.assertFalse( + "virtualColumns field should not appear in JSON when null", + jsonWithNull.contains("virtualColumns") + ); + } + + @Test + public void testSerde_emptyVirtualColumns_deserializesToEmptyNotNull() throws IOException + { + final ObjectMapper mapper = new DefaultObjectMapper(); + + // JSON without virtualColumns field (like old data) + final String jsonWithoutField = "{\"filter\":{\"type\":\"selector\",\"dimension\":\"dim1\",\"value\":\"foo\"}}"; + + final CompactionTransformSpec deserialized = mapper.readValue( + jsonWithoutField, + CompactionTransformSpec.class + ); + + // Should deserialize to VirtualColumns.EMPTY, not null + Assert.assertNotNull("virtualColumns should not be null after deserialization", deserialized.getVirtualColumns()); + Assert.assertEquals("virtualColumns should be EMPTY", VirtualColumns.EMPTY, deserialized.getVirtualColumns()); + Assert.assertTrue("virtualColumns should be empty", deserialized.getVirtualColumns().isEmpty()); + } } From e560a979b0bea2c3f9ab6354a3ef74238a4feb51 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 00:53:38 -0600 Subject: [PATCH 80/90] All P0D older than to essentially make all data from referance and older eligible for compaction --- .../CascadingReindexingTemplateTest.java | 159 ++++++++++++++++++ .../compaction/AbstractReindexingRule.java | 57 +++---- .../ReindexingDeletionRuleTest.java | 27 ++- .../ReindexingIOConfigRuleTest.java | 12 +- .../ReindexingSegmentGranularityRuleTest.java | 14 +- .../ReindexingTuningConfigRuleTest.java | 12 +- 6 files changed, 228 insertions(+), 53 deletions(-) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 40b44d77acab..14ebf40d5461 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -1223,6 +1223,165 @@ public void test_generateAlignedSearchIntervals_singleRuleOnly() Assert.assertEquals(expected, actual); } + /** + * TEST: Zero period rule (P0D) applies immediately to all data + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P0D→HOUR (applies to all data immediately)
    • + *
    • Other Rules: None
    • + *
    • Default Segment Granularity: DAY
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Synthetic Rules: None
    2. + *
    3. Initial Timeline: [-∞, 2025-01-29T16:00:00) - HOUR (P0D means threshold equals reference time)
    4. + *
    5. Timeline Splits: None (no non-segment-gran rules)
    6. + *
    + *

    + * EXPECTED OUTPUT: 1 interval + *

      + *
    1. [-∞, 2025-01-29T16:00:00) - HOUR (aligned to hour boundary at reference time)
    2. + *
    + */ + @Test + public void test_generateAlignedSearchIntervals_zeroPeriodRuleAppliesImmediately() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingSegmentGranularityRule hourRule = new ReindexingSegmentGranularityRule( + "immediate-hour-rule", + "Apply HOUR granularity to all data immediately", + Period.days(0), + Granularities.HOUR + ); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of(hourRule)) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2025-01-29T16:00:00Z")), + Granularities.HOUR, + hourRule + ) + ); + + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); + } + + /** + * TEST: Zero period rule (P0D) with other rules creates proper cascading timeline + *

    + * REFERENCE TIME: 2025-01-29T16:15:00Z + *

    + * INPUT RULES: + *

      + *
    • Segment Granularity Rules: P0D→HOUR, P30D→DAY, P90D→MONTH
    • + *
    • Other Rules: None
    • + *
    • Default Segment Granularity: DAY
    • + *
    + *

    + * PROCESSING: + *

      + *
    1. Synthetic Rules: None
    2. + *
    3. Sort rules by period: P90D (oldest), P30D (middle), P0D (newest/applies immediately)
    4. + *
    5. Initial Timeline: + *
        + *
      • P90D → MONTH: Raw 2024-10-31T16:15 → Aligned 2024-10-01T00:00
      • + *
      • P30D → DAY: Raw 2024-12-30T16:15 → Aligned 2024-12-30T00:00
      • + *
      • P0D → HOUR: Raw 2025-01-29T16:15 → Aligned 2025-01-29T16:00
      • + *
      + *
    6. + *
    7. Timeline Splits: None (no non-segment-gran rules)
    8. + *
    + *

    + * EXPECTED OUTPUT: 3 intervals + *

      + *
    1. [-∞, 2024-10-01T00:00:00) - MONTH
    2. + *
    3. [2024-10-01T00:00:00, 2024-12-30T00:00:00) - DAY
    4. + *
    5. [2024-12-30T00:00:00, 2025-01-29T16:00:00) - HOUR
    6. + *
    + */ + @Test + public void test_generateAlignedSearchIntervals_zeroPeriodRuleWithOtherRules() + { + DateTime referenceTime = DateTimes.of("2025-01-29T16:15:00Z"); + + ReindexingSegmentGranularityRule monthRule = new ReindexingSegmentGranularityRule( + "month-rule", + null, + Period.days(90), + Granularities.MONTH + ); + ReindexingSegmentGranularityRule dayRule = new ReindexingSegmentGranularityRule( + "day-rule", + null, + Period.days(30), + Granularities.DAY + ); + ReindexingSegmentGranularityRule hourRule = new ReindexingSegmentGranularityRule( + "immediate-hour-rule", + "Apply HOUR granularity immediately", + Period.days(0), + Granularities.HOUR + ); + + ReindexingRuleProvider provider = InlineReindexingRuleProvider.builder() + .segmentGranularityRules(List.of(hourRule, dayRule, monthRule)) + .build(); + + CascadingReindexingTemplate template = new CascadingReindexingTemplate( + "testDS", + null, + null, + provider, + null, + null, + null, + null, + Granularities.DAY + ); + + List expected = List.of( + new IntervalGranularityInfo( + new Interval(DateTimes.MIN, DateTimes.of("2024-10-01T00:00:00Z")), + Granularities.MONTH, + monthRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-10-01T00:00:00Z"), DateTimes.of("2024-12-30T00:00:00Z")), + Granularities.DAY, + dayRule + ), + new IntervalGranularityInfo( + new Interval(DateTimes.of("2024-12-30T00:00:00Z"), DateTimes.of("2025-01-29T16:00:00Z")), + Granularities.HOUR, + hourRule + ) + ); + + List actual = template.generateAlignedSearchIntervals(referenceTime); + Assert.assertEquals(expected, actual); + } + /** * TEST: Validation failure - default granularity is coarser than most recent segment granularity rule *

    diff --git a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java index a3b0eaf61a67..3131df31cbbf 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/AbstractReindexingRule.java @@ -59,59 +59,54 @@ public AbstractReindexingRule( this.description = description; this.olderThan = Objects.requireNonNull(olderThan, "olderThan period cannot be null"); - validatePeriodIsPositive(olderThan); + validatePeriodIsNonNegative(olderThan); } /** - * Validates that a period represents a positive duration. + * Validates that a period represents a non-negative duration (>= 0). + *

    + * Zero periods (P0D) are allowed - they indicate rules that should apply immediately to all data. + * Negative periods are rejected as they would be nonsensical. *

    * For periods with precise units (days, hours, minutes, seconds), validates by converting * to a standard duration. For periods with variable-length units (months, years), validates - * that at least one component is positive, since these cannot be converted to a precise duration. + * that no components are negative, since these cannot be converted to a precise duration. * * @param period the period to validate - * @throws IllegalArgumentException if the period is not positive + * @throws IllegalArgumentException if the period is negative */ - private static void validatePeriodIsPositive(Period period) + private static void validatePeriodIsNonNegative(Period period) { if (hasMonthsOrYears(period)) { - if (!isPeriodPositive(period)) { - throw new IllegalArgumentException("period must be positive. Supplied period: " + period); + if (isPeriodNegative(period)) { + throw new IllegalArgumentException("period must not be negative. Supplied period: " + period); } } else { - if (period.toStandardDuration().getMillis() <= 0) { - throw new IllegalArgumentException("period must be positive. Supplied period: " + period); + if (period.toStandardDuration().getMillis() < 0) { + throw new IllegalArgumentException("period must not be negative. Supplied period: " + period); } } } /** - * Checks if a period with variable-length components (months/years) is positive. + * Checks if a period with variable-length components (months/years) has any negative components. + *

    + * This is purposely an unscientific check that simply ensures no negative values are present in any component of the period. + * It should be "good enough" for almost all reasonable use cases. * * @param period the period to check - * @return true if any component is positive and no components are negative + * @return true if any component is negative */ - private static boolean isPeriodPositive(Period period) + private static boolean isPeriodNegative(Period period) { - boolean hasPositiveComponent = period.getYears() > 0 - || period.getMonths() > 0 - || period.getWeeks() > 0 - || period.getDays() > 0 - || period.getHours() > 0 - || period.getMinutes() > 0 - || period.getSeconds() > 0 - || period.getMillis() > 0; - - boolean hasNegativeComponent = period.getYears() < 0 - || period.getMonths() < 0 - || period.getWeeks() < 0 - || period.getDays() < 0 - || period.getHours() < 0 - || period.getMinutes() < 0 - || period.getSeconds() < 0 - || period.getMillis() < 0; - - return hasPositiveComponent && !hasNegativeComponent; + return period.getYears() < 0 + || period.getMonths() < 0 + || period.getWeeks() < 0 + || period.getDays() < 0 + || period.getHours() < 0 + || period.getMinutes() < 0 + || period.getSeconds() < 0 + || period.getMillis() < 0; } @JsonProperty diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java index cc93ec66cce0..4204eab20420 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java @@ -163,13 +163,18 @@ public void test_constructor_nullPeriod_throwsNullPointerException() } @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + public void test_constructor_zeroPeriod_succeeds() { + // P0D is valid - indicates rules that apply immediately to all data Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingDeletionRule("test-id", "description", zeroPeriod, testFilter, null) + ReindexingDeletionRule rule = new ReindexingDeletionRule( + "test-id", + "description", + zeroPeriod, + testFilter, + null ); + Assert.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test @@ -258,14 +263,18 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds() } @Test - public void test_constructor_zeroMonthsPeriod_throwsIllegalArgumentException() + public void test_constructor_zeroMonthsPeriod_succeeds() { - // P0M should fail - all components are zero/non-positive + // P0M is valid - equivalent to P0D, indicates rules that apply immediately to all data Period zeroPeriod = Period.months(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingDeletionRule("test-id", "description", zeroPeriod, testFilter, null) + ReindexingDeletionRule rule = new ReindexingDeletionRule( + "test-id", + "description", + zeroPeriod, + testFilter, + null ); + Assert.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index 3913aeeab85b..c4f215e3ab58 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -135,14 +135,18 @@ public void test_constructor_nullPeriod_throwsNullPointerException() } @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + public void test_constructor_zeroPeriod_succeeds() { + // P0D is valid - indicates rules that apply immediately to all data UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingIOConfigRule("test-id", "description", zeroPeriod, config) + ReindexingIOConfigRule rule = new ReindexingIOConfigRule( + "test-id", + "description", + zeroPeriod, + config ); + Assert.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java index 9aaf184afe1e..ef58e61e2c3c 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java @@ -138,13 +138,17 @@ public void test_constructor_nullPeriod_throwsNullPointerException() } @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + public void test_constructor_zeroPeriod_succeeds() { + // P0D is valid - indicates rules that apply immediately to all data Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingSegmentGranularityRule("test-id", "description", zeroPeriod, Granularities.HOUR) + ReindexingSegmentGranularityRule rule = new ReindexingSegmentGranularityRule( + "test-id", + "description", + zeroPeriod, + Granularities.HOUR ); + Assert.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test @@ -199,7 +203,7 @@ public void test_constructor_unsupportedGranularities_allThrowDruidException() Granularities.EIGHT_HOUR, Granularities.WEEK, new PeriodGranularity(Period.days(3), null, DateTimeZone.UTC), // Custom period - new PeriodGranularity(Period.days(1), null, DateTimeZone.forID("America/Los_Angeles")) // With timezone + new PeriodGranularity(Period.days(1), null, DateTimes.inferTzFromString("America/Los_Angeles")) // With timezone }; for (Granularity granularity : unsupportedGranularities) { diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index 7ef907679a60..811f4e187ad3 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -136,13 +136,17 @@ public void test_constructor_nullPeriod_throwsNullPointerException() } @Test - public void test_constructor_zeroPeriod_throwsIllegalArgumentException() + public void test_constructor_zeroPeriod_succeeds() { + // P0D is valid - indicates rules that apply immediately to all data Period zeroPeriod = Period.days(0); - Assert.assertThrows( - IllegalArgumentException.class, - () -> new ReindexingTuningConfigRule("test-id", "description", zeroPeriod, createTestTuningConfig()) + ReindexingTuningConfigRule rule = new ReindexingTuningConfigRule( + "test-id", + "description", + zeroPeriod, + createTestTuningConfig() ); + Assert.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test From 408208cf119484476cc22aa02b171daacb75970b Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 10:04:44 -0600 Subject: [PATCH 81/90] fix test that did not get updated when CompactionTransformSpec updated virtual columns default to EMPTY --- .../indexing/compact/ReindexingDeletionRuleOptimizerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java index 5f4eafb0f878..40fd6ca4a6dc 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java @@ -488,8 +488,7 @@ public void testOptimize_FilterVirtualColumns_NoColumnsReferenced() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - // Virtual columns should be null when no virtual columns are referenced - Assert.assertNull(result.getTransformSpec().getVirtualColumns()); + Assert.assertEquals(VirtualColumns.EMPTY, result.getTransformSpec().getVirtualColumns()); } @Test From 16b25af35f089b8a1364fd8e2b24f29e61dbc0b9 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 10:08:42 -0600 Subject: [PATCH 82/90] Fix improper hashing in the data schema rule --- .../druid/server/compaction/ReindexingDataSchemaRule.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java index 7c5c25c78ed6..5a9f94f01492 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java @@ -128,16 +128,17 @@ public boolean equals(Object o) @Override public int hashCode() { - return Objects.hash( + int result = Objects.hash( getId(), getDescription(), getOlderThan(), dimensionsSpec, - Objects.hashCode(metricsSpec), queryGranularity, rollup, projections ); + result = 31 * result + Arrays.hashCode(metricsSpec); + return result; } @Override From 216d984dbb1e529436a432f7563600bd2d4a0bc6 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 11:15:13 -0600 Subject: [PATCH 83/90] move rule tests to junit5 --- .../AbstractReindexingRuleTest.java | 37 +- .../ReindexingDataSchemaRuleTest.java | 351 ++++++++++++++++++ .../ReindexingDeletionRuleTest.java | 61 +-- .../ReindexingIOConfigRuleTest.java | 30 +- .../ReindexingSegmentGranularityRuleTest.java | 42 +-- .../ReindexingTuningConfigRuleTest.java | 32 +- 6 files changed, 456 insertions(+), 97 deletions(-) create mode 100644 server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java diff --git a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java index c0e6fb344a8b..e6b55c406bed 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/AbstractReindexingRuleTest.java @@ -21,35 +21,42 @@ import org.apache.druid.query.filter.SelectorDimFilter; import org.joda.time.Period; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class AbstractReindexingRuleTest { - @Test(expected = IllegalArgumentException.class) + @Test public void test_constructor_positiveMonthsNegativeDays_throwsException() { Period period = Period.months(1).withDays(-40); - new ReindexingDeletionRule( - "test-rule", - null, - period, - new SelectorDimFilter("dim", "val", null), - null + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReindexingDeletionRule( + "test-rule", + null, + period, + new SelectorDimFilter("dim", "val", null), + null + ) ); } - @Test(expected = IllegalArgumentException.class) + @Test public void test_constructor_positiveYearsNegativeMonths_throwsException() { Period period = new Period(1, -13, 0, 0, 0, 0, 0, 0); - new ReindexingDeletionRule( - "test-rule", - null, - period, - new SelectorDimFilter("dim", "val", null), - null + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReindexingDeletionRule( + "test-rule", + null, + period, + new SelectorDimFilter("dim", "val", null), + null + ) ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java new file mode 100644 index 000000000000..8f48d8330b37 --- /dev/null +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java @@ -0,0 +1,351 @@ +/* + * 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.compaction; + +import org.apache.druid.data.input.impl.LongDimensionSchema; +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.granularity.Granularity; +import org.apache.druid.query.aggregation.AggregatorFactory; +import org.apache.druid.query.aggregation.CountAggregatorFactory; +import org.apache.druid.query.aggregation.LongSumAggregatorFactory; +import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; +import org.joda.time.DateTime; +import org.joda.time.Interval; +import org.joda.time.Period; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.List; + +public class ReindexingDataSchemaRuleTest +{ + private static final DateTime REFERENCE_TIME = DateTimes.of("2025-12-19T12:00:00Z"); + private static final Period PERIOD_14_DAYS = Period.days(14); + + private final UserCompactionTaskDimensionsConfig dimensionsSpec = new UserCompactionTaskDimensionsConfig(List.of(new LongDimensionSchema("dim1"))); + private final AggregatorFactory[] metricsSpec = new AggregatorFactory[]{ + new CountAggregatorFactory("count"), + new LongSumAggregatorFactory("sum_metric", "metric") + }; + private final Granularity queryGranularity = Granularities.MINUTE; + private final Boolean rollup = true; + + private final ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-schema-rule", + "Custom data schema", + PERIOD_14_DAYS, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ); + + @Test + public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval ends at 2025-12-03, which is fully before threshold + Interval interval = Intervals.of("2025-12-02T00:00:00Z/2025-12-03T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalEndsAtThreshold_returnsFull() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval ends exactly at threshold - should be FULL (boundary case) + Interval interval = Intervals.of("2025-12-04T12:00:00Z/2025-12-05T12:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + } + + @Test + public void test_appliesTo_intervalSpansThreshold_returnsPartial() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval starts before threshold and ends after - PARTIAL + Interval interval = Intervals.of("2025-12-04T00:00:00Z/2025-12-06T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assertions.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + } + + @Test + public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() + { + // Threshold is 2025-12-05T12:00:00Z (14 days before reference time) + // Interval starts after threshold - NONE + Interval interval = Intervals.of("2025-12-15T00:00:00Z/2025-12-16T00:00:00Z"); + + ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); + + Assertions.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + } + + @Test + public void test_getDimensionsSpec_returnsConfiguredValue() + { + UserCompactionTaskDimensionsConfig dimensions = rule.getDimensionsSpec(); + + Assertions.assertNotNull(dimensions); + Assertions.assertEquals(dimensionsSpec, dimensions); + } + + @Test + public void test_getMetricsSpec_returnsConfiguredValue() + { + AggregatorFactory[] metrics = rule.getMetricsSpec(); + + Assertions.assertNotNull(metrics); + Assertions.assertEquals(2, metrics.length); + Assertions.assertEquals("count", metrics[0].getName()); + Assertions.assertEquals("sum_metric", metrics[1].getName()); + } + + @Test + public void test_getQueryGranularity_returnsConfiguredValue() + { + Granularity granularity = rule.getQueryGranularity(); + + Assertions.assertNotNull(granularity); + Assertions.assertEquals(queryGranularity, granularity); + } + + @Test + public void test_getRollup_returnsConfiguredValue() + { + Boolean rollupValue = rule.getRollup(); + + Assertions.assertNotNull(rollupValue); + Assertions.assertEquals(true, rollupValue); + } + + @Test + public void test_getProjections_returnsConfiguredValue() + { + Assertions.assertNotNull(rule.getProjections()); + Assertions.assertEquals(Collections.emptyList(), rule.getProjections()); + } + + @Test + public void test_getId_returnsConfiguredId() + { + Assertions.assertEquals("test-schema-rule", rule.getId()); + } + + @Test + public void test_getDescription_returnsConfiguredDescription() + { + Assertions.assertEquals("Custom data schema", rule.getDescription()); + } + + @Test + public void test_getOlderThan_returnsConfiguredPeriod() + { + Assertions.assertEquals(PERIOD_14_DAYS, rule.getOlderThan()); + } + + @Test + public void test_constructor_nullId_throwsNullPointerException() + { + Assertions.assertThrows( + NullPointerException.class, + () -> new ReindexingDataSchemaRule( + null, + "description", + PERIOD_14_DAYS, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ) + ); + } + + @Test + public void test_constructor_nullPeriod_throwsNullPointerException() + { + Assertions.assertThrows( + NullPointerException.class, + () -> new ReindexingDataSchemaRule( + "test-id", + "description", + null, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ) + ); + } + + @Test + public void test_constructor_zeroPeriod_succeeds() + { + // P0D is valid - indicates rules that apply immediately to all data + Period zeroPeriod = Period.days(0); + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + zeroPeriod, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); + } + + @Test + public void test_constructor_negativePeriod_throwsIllegalArgumentException() + { + Period negativePeriod = Period.days(-14); + Assertions.assertThrows( + IllegalArgumentException.class, + () -> new ReindexingDataSchemaRule( + "test-id", + "description", + negativePeriod, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ) + ); + } + + @Test + public void test_constructor_nullDimensionsSpec_succeeds() + { + // Null dimensions spec is allowed + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + null, + metricsSpec, + queryGranularity, + rollup, + Collections.emptyList() + ); + Assertions.assertNull(rule.getDimensionsSpec()); + } + + @Test + public void test_constructor_nullMetricsSpec_succeeds() + { + // Null metrics spec is allowed + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + dimensionsSpec, + null, + queryGranularity, + rollup, + Collections.emptyList() + ); + Assertions.assertNull(rule.getMetricsSpec()); + } + + @Test + public void test_constructor_nullQueryGranularity_succeeds() + { + // Null query granularity is allowed + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + dimensionsSpec, + metricsSpec, + null, + rollup, + Collections.emptyList() + ); + Assertions.assertNull(rule.getQueryGranularity()); + } + + @Test + public void test_constructor_nullRollup_succeeds() + { + // Null rollup is allowed + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + dimensionsSpec, + metricsSpec, + queryGranularity, + null, + Collections.emptyList() + ); + Assertions.assertNull(rule.getRollup()); + } + + @Test + public void test_constructor_nullProjections_succeeds() + { + // Null projections is allowed + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + dimensionsSpec, + metricsSpec, + queryGranularity, + rollup, + null + ); + Assertions.assertNull(rule.getProjections()); + } + + @Test + public void test_constructor_emptyMetricsSpec_succeeds() + { + // Empty metrics array is allowed + AggregatorFactory[] emptyMetrics = new AggregatorFactory[0]; + ReindexingDataSchemaRule rule = new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + dimensionsSpec, + emptyMetrics, + queryGranularity, + rollup, + Collections.emptyList() + ); + Assertions.assertNotNull(rule.getMetricsSpec()); + Assertions.assertEquals(0, rule.getMetricsSpec().length); + } +} diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java index 4204eab20420..becd915c9aa6 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java @@ -31,8 +31,9 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + public class ReindexingDeletionRuleTest { @@ -69,7 +70,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -81,7 +82,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -93,7 +94,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -105,7 +106,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -113,8 +114,8 @@ public void test_getVirtualColumns_returnsConfiguredVirtualColumns() { VirtualColumns vCols = rule.getVirtualColumns(); - Assert.assertNotNull(vCols); - Assert.assertEquals(virtualColumns, vCols); + Assertions.assertNotNull(vCols); + Assertions.assertEquals(virtualColumns, vCols); } @Test @@ -122,32 +123,32 @@ public void test_getDeleteWhere_returnsConfiguredFilter() { DimFilter filter = rule.getDeleteWhere(); - Assert.assertNotNull(filter); - Assert.assertEquals(testFilter, filter); + Assertions.assertNotNull(filter); + Assertions.assertEquals(testFilter, filter); } @Test public void test_getId_returnsConfiguredId() { - Assert.assertEquals("test-filter-rule", rule.getId()); + Assertions.assertEquals("test-filter-rule", rule.getId()); } @Test public void test_getDescription_returnsConfiguredDescription() { - Assert.assertEquals("Remove robot traffic", rule.getDescription()); + Assertions.assertEquals("Remove robot traffic", rule.getDescription()); } @Test public void test_getOlderThan_returnsConfiguredPeriod() { - Assert.assertEquals(PERIOD_30_DAYS, rule.getOlderThan()); + Assertions.assertEquals(PERIOD_30_DAYS, rule.getOlderThan()); } @Test public void test_constructor_nullId_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingDeletionRule(null, "description", PERIOD_30_DAYS, testFilter, null) ); @@ -156,7 +157,7 @@ public void test_constructor_nullId_throwsNullPointerException() @Test public void test_constructor_nullPeriod_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingDeletionRule("test-id", "description", null, testFilter, null) ); @@ -174,14 +175,14 @@ public void test_constructor_zeroPeriod_succeeds() testFilter, null ); - Assert.assertEquals(zeroPeriod, rule.getOlderThan()); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test public void test_constructor_negativePeriod_throwsIllegalArgumentException() { Period negativePeriod = Period.days(-30); - Assert.assertThrows( + Assertions.assertThrows( IllegalArgumentException.class, () -> new ReindexingDeletionRule("test-id", "description", negativePeriod, testFilter, null) ); @@ -190,7 +191,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() @Test public void test_constructor_nullDeleteWhere_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingDeletionRule("test-id", "description", PERIOD_30_DAYS, null, null) ); @@ -211,7 +212,7 @@ public void test_constructor_periodWithMonths_succeeds() null ); - Assert.assertEquals(period, rule.getOlderThan()); + Assertions.assertEquals(period, rule.getOlderThan()); } @Test @@ -227,7 +228,7 @@ public void test_constructor_periodWithYears_succeeds() null ); - Assert.assertEquals(period, rule.getOlderThan()); + Assertions.assertEquals(period, rule.getOlderThan()); } @Test @@ -243,7 +244,7 @@ public void test_constructor_periodWithMixedMonthsAndDays_succeeds() null ); - Assert.assertEquals(period, rule.getOlderThan()); + Assertions.assertEquals(period, rule.getOlderThan()); } @Test @@ -259,7 +260,7 @@ public void test_constructor_periodWithYearsMonthsDays_succeeds() null ); - Assert.assertEquals(period, rule.getOlderThan()); + Assertions.assertEquals(period, rule.getOlderThan()); } @Test @@ -274,7 +275,7 @@ public void test_constructor_zeroMonthsPeriod_succeeds() testFilter, null ); - Assert.assertEquals(zeroPeriod, rule.getOlderThan()); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test @@ -282,7 +283,7 @@ public void test_constructor_negativeMonthsPeriod_throwsIllegalArgumentException { // P-6M should fail - negative months Period negativePeriod = Period.months(-6); - Assert.assertThrows( + Assertions.assertThrows( IllegalArgumentException.class, () -> new ReindexingDeletionRule("test-id", "description", negativePeriod, testFilter, null) ); @@ -307,21 +308,21 @@ public void test_appliesTo_periodWithMonths_calculatesThresholdCorrectly() // Interval ending before 6-month threshold - should be FULL Interval beforeThreshold = Intervals.of("2025-06-01T00:00:00Z/2025-06-15T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.FULL, monthRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 6-month threshold - should be PARTIAL Interval spanningThreshold = Intervals.of("2025-06-15T00:00:00Z/2025-07-15T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.PARTIAL, monthRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 6-month threshold - should be NONE Interval afterThreshold = Intervals.of("2025-07-01T00:00:00Z/2025-07-15T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.NONE, monthRule.appliesTo(afterThreshold, REFERENCE_TIME) ); @@ -346,21 +347,21 @@ public void test_appliesTo_periodWithYears_calculatesThresholdCorrectly() // Interval ending before 1-year threshold - should be FULL Interval beforeThreshold = Intervals.of("2024-11-01T00:00:00Z/2024-12-01T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.FULL, yearRule.appliesTo(beforeThreshold, REFERENCE_TIME) ); // Interval spanning the 1-year threshold - should be PARTIAL Interval spanningThreshold = Intervals.of("2024-12-01T00:00:00Z/2025-01-01T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.PARTIAL, yearRule.appliesTo(spanningThreshold, REFERENCE_TIME) ); // Interval starting after 1-year threshold - should be NONE Interval afterThreshold = Intervals.of("2025-01-01T00:00:00Z/2025-02-01T00:00:00Z"); - Assert.assertEquals( + Assertions.assertEquals( ReindexingRule.AppliesToMode.NONE, yearRule.appliesTo(afterThreshold, REFERENCE_TIME) ); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index c4f215e3ab58..472d8f9f1b5f 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -25,8 +25,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class ReindexingIOConfigRuleTest { @@ -49,7 +49,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -61,7 +61,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -73,7 +73,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -85,7 +85,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -93,32 +93,32 @@ public void test_getIoConfig_returnsConfiguredValue() { UserCompactionTaskIOConfig config = rule.getIoConfig(); - Assert.assertNotNull(config); + Assertions.assertNotNull(config); } @Test public void test_getId_returnsConfiguredId() { - Assert.assertEquals("test-ioconfig-rule", rule.getId()); + Assertions.assertEquals("test-ioconfig-rule", rule.getId()); } @Test public void test_getDescription_returnsConfiguredDescription() { - Assert.assertEquals("Custom IO config", rule.getDescription()); + Assertions.assertEquals("Custom IO config", rule.getDescription()); } @Test public void test_getOlderThan_returnsConfiguredPeriod() { - Assert.assertEquals(PERIOD_60_DAYS, rule.getOlderThan()); + Assertions.assertEquals(PERIOD_60_DAYS, rule.getOlderThan()); } @Test public void test_constructor_nullId_throwsNullPointerException() { UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingIOConfigRule(null, "description", PERIOD_60_DAYS, config) ); @@ -128,7 +128,7 @@ public void test_constructor_nullId_throwsNullPointerException() public void test_constructor_nullPeriod_throwsNullPointerException() { UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingIOConfigRule("test-id", "description", null, config) ); @@ -146,7 +146,7 @@ public void test_constructor_zeroPeriod_succeeds() zeroPeriod, config ); - Assert.assertEquals(zeroPeriod, rule.getOlderThan()); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test @@ -154,7 +154,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() { UserCompactionTaskIOConfig config = new UserCompactionTaskIOConfig(null); Period negativePeriod = Period.days(-60); - Assert.assertThrows( + Assertions.assertThrows( IllegalArgumentException.class, () -> new ReindexingIOConfigRule("test-id", "description", negativePeriod, config) ); @@ -163,7 +163,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() @Test public void test_constructor_nullIOConfig_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingIOConfigRule("test-id", "description", PERIOD_60_DAYS, null) ); diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java index ef58e61e2c3c..9fa076d69ae0 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingSegmentGranularityRuleTest.java @@ -29,8 +29,8 @@ import org.joda.time.DateTimeZone; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class ReindexingSegmentGranularityRuleTest { @@ -53,7 +53,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -65,7 +65,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -77,7 +77,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -89,7 +89,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -97,32 +97,32 @@ public void test_getGranularity_returnsConfiguredValue() { Granularity granularity = rule.getSegmentGranularity(); - Assert.assertNotNull(granularity); - Assert.assertEquals(Granularities.HOUR, granularity); + Assertions.assertNotNull(granularity); + Assertions.assertEquals(Granularities.HOUR, granularity); } @Test public void test_getId_returnsConfiguredId() { - Assert.assertEquals("test-rule", rule.getId()); + Assertions.assertEquals("test-rule", rule.getId()); } @Test public void test_getDescription_returnsConfiguredDescription() { - Assert.assertEquals("Test segment granularity rule", rule.getDescription()); + Assertions.assertEquals("Test segment granularity rule", rule.getDescription()); } @Test public void test_getOlderThan_returnsConfiguredPeriod() { - Assert.assertEquals(PERIOD_7_DAYS, rule.getOlderThan()); + Assertions.assertEquals(PERIOD_7_DAYS, rule.getOlderThan()); } @Test public void test_constructor_nullId_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingSegmentGranularityRule(null, "description", PERIOD_7_DAYS, Granularities.HOUR) ); @@ -131,7 +131,7 @@ public void test_constructor_nullId_throwsNullPointerException() @Test public void test_constructor_nullPeriod_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingSegmentGranularityRule("test-id", "description", null, Granularities.HOUR) ); @@ -148,14 +148,14 @@ public void test_constructor_zeroPeriod_succeeds() zeroPeriod, Granularities.HOUR ); - Assert.assertEquals(zeroPeriod, rule.getOlderThan()); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test public void test_constructor_negativePeriod_throwsIllegalArgumentException() { Period negativePeriod = Period.days(-7); - Assert.assertThrows( + Assertions.assertThrows( IllegalArgumentException.class, () -> new ReindexingSegmentGranularityRule("test-id", "description", negativePeriod, Granularities.HOUR) ); @@ -164,7 +164,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() @Test public void test_constructor_nullGranularity_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingSegmentGranularityRule("test-id", "description", PERIOD_7_DAYS, null) ); @@ -190,7 +190,7 @@ public void test_constructor_supportedGranularities_allSucceed() PERIOD_7_DAYS, granularity ); - Assert.assertEquals(granularity, rule.getSegmentGranularity()); + Assertions.assertEquals(granularity, rule.getSegmentGranularity()); } } @@ -207,13 +207,13 @@ public void test_constructor_unsupportedGranularities_allThrowDruidException() }; for (Granularity granularity : unsupportedGranularities) { - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> new ReindexingSegmentGranularityRule("test-id", "description", PERIOD_7_DAYS, granularity) ); - Assert.assertTrue( - "Expected exception message to contain 'Unsupported segment granularity' but got: " + exception.getMessage(), - exception.getMessage().contains("Unsupported segment granularity") + Assertions.assertTrue( + exception.getMessage().contains("Unsupported segment granularity"), + "Expected exception message to contain 'Unsupported segment granularity' but got: " + exception.getMessage() ); } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index 811f4e187ad3..67e5d7c4b3a9 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -26,8 +26,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class ReindexingTuningConfigRuleTest { @@ -51,7 +51,7 @@ public void test_appliesTo_intervalFullyBeforeThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -63,7 +63,7 @@ public void test_appliesTo_intervalEndsAtThreshold_returnsFull() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.FULL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.FULL, result); } @Test @@ -75,7 +75,7 @@ public void test_appliesTo_intervalSpansThreshold_returnsPartial() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.PARTIAL, result); } @Test @@ -87,7 +87,7 @@ public void test_appliesTo_intervalStartsAfterThreshold_returnsNone() ReindexingRule.AppliesToMode result = rule.appliesTo(interval, REFERENCE_TIME); - Assert.assertEquals(ReindexingRule.AppliesToMode.NONE, result); + Assertions.assertEquals(ReindexingRule.AppliesToMode.NONE, result); } @Test @@ -95,32 +95,32 @@ public void test_getTuningConfig_returnsConfiguredValue() { UserCompactionTaskQueryTuningConfig config = rule.getTuningConfig(); - Assert.assertNotNull(config); - Assert.assertNotNull(config.getPartitionsSpec()); + Assertions.assertNotNull(config); + Assertions.assertNotNull(config.getPartitionsSpec()); } @Test public void test_getId_returnsConfiguredId() { - Assert.assertEquals("test-tuning-rule", rule.getId()); + Assertions.assertEquals("test-tuning-rule", rule.getId()); } @Test public void test_getDescription_returnsConfiguredDescription() { - Assert.assertEquals("Custom tuning config", rule.getDescription()); + Assertions.assertEquals("Custom tuning config", rule.getDescription()); } @Test public void test_getOlderThan_returnsConfiguredPeriod() { - Assert.assertEquals(PERIOD_21_DAYS, rule.getOlderThan()); + Assertions.assertEquals(PERIOD_21_DAYS, rule.getOlderThan()); } @Test public void test_constructor_nullId_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingTuningConfigRule(null, "description", PERIOD_21_DAYS, createTestTuningConfig()) ); @@ -129,7 +129,7 @@ public void test_constructor_nullId_throwsNullPointerException() @Test public void test_constructor_nullPeriod_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingTuningConfigRule("test-id", "description", null, createTestTuningConfig()) ); @@ -146,14 +146,14 @@ public void test_constructor_zeroPeriod_succeeds() zeroPeriod, createTestTuningConfig() ); - Assert.assertEquals(zeroPeriod, rule.getOlderThan()); + Assertions.assertEquals(zeroPeriod, rule.getOlderThan()); } @Test public void test_constructor_negativePeriod_throwsIllegalArgumentException() { Period negativePeriod = Period.days(-21); - Assert.assertThrows( + Assertions.assertThrows( IllegalArgumentException.class, () -> new ReindexingTuningConfigRule("test-id", "description", negativePeriod, createTestTuningConfig()) ); @@ -162,7 +162,7 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() @Test public void test_constructor_nullTuningConfig_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ReindexingTuningConfigRule("test-id", "description", PERIOD_21_DAYS, null) ); From 619a0d1f127962be0352482df9b70c9b20b7ac6e Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 11:25:30 -0600 Subject: [PATCH 84/90] migrate the rule provider test files to junit5 --- .../ComposingReindexingRuleProviderTest.java | 58 ++++++++-------- .../InlineReindexingRuleProviderTest.java | 68 +++++++++---------- 2 files changed, 63 insertions(+), 63 deletions(-) diff --git a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java index 7dae689f19ae..03ce574273e1 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ComposingReindexingRuleProviderTest.java @@ -33,8 +33,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Collections; @@ -49,7 +49,7 @@ public class ComposingReindexingRuleProviderTest @Test public void test_constructor_nullProviders_throwsNullPointerException() { - Assert.assertThrows( + Assertions.assertThrows( NullPointerException.class, () -> new ComposingReindexingRuleProvider(null) ); @@ -62,12 +62,12 @@ public void test_constructor_nullProviderInList_throwsNullPointerException() providers.add(InlineReindexingRuleProvider.builder().build()); providers.add(null); // Null provider - NullPointerException exception = Assert.assertThrows( + NullPointerException exception = Assertions.assertThrows( NullPointerException.class, () -> new ComposingReindexingRuleProvider(providers) ); - Assert.assertTrue(exception.getMessage().contains("providers list contains null element")); + Assertions.assertTrue(exception.getMessage().contains("providers list contains null element")); } @Test @@ -77,9 +77,9 @@ public void test_constructor_emptyProviderList_succeeds() Collections.emptyList() ); - Assert.assertEquals("composing", composing.getType()); - Assert.assertTrue(composing.isReady()); - Assert.assertTrue(composing.getDeletionRules().isEmpty()); + Assertions.assertEquals("composing", composing.getType()); + Assertions.assertTrue(composing.isReady()); + Assertions.assertTrue(composing.getDeletionRules().isEmpty()); } @@ -93,7 +93,7 @@ public void test_isReady_allProvidersReady_returnsTrue() ImmutableList.of(provider1, provider2) ); - Assert.assertTrue(composing.isReady()); + Assertions.assertTrue(composing.isReady()); } @Test @@ -106,7 +106,7 @@ public void test_isReady_someProvidersNotReady_returnsFalse() ImmutableList.of(readyProvider, notReadyProvider) ); - Assert.assertFalse(composing.isReady()); + Assertions.assertFalse(composing.isReady()); } @Test @@ -116,7 +116,7 @@ public void test_isReady_emptyProviderList_returnsTrue() Collections.emptyList() ); - Assert.assertTrue(composing.isReady()); + Assertions.assertTrue(composing.isReady()); } @Test @@ -254,8 +254,8 @@ public void test_equals_sameProviders_returnsTrue() ImmutableList.of(provider1, provider2) ); - Assert.assertEquals(composing1, composing2); - Assert.assertEquals(composing1.hashCode(), composing2.hashCode()); + Assertions.assertEquals(composing1, composing2); + Assertions.assertEquals(composing1.hashCode(), composing2.hashCode()); } @Test @@ -273,7 +273,7 @@ public void test_equals_differentProviders_returnsFalse() ImmutableList.of(provider1, provider2) ); - Assert.assertNotEquals(composing1, composing2); + Assertions.assertNotEquals(composing1, composing2); } @@ -315,8 +315,8 @@ private void testComposingBehaviorForRuleType( ); List result = ruleGetter.apply(composing); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", idExtractor.apply(result.get(0))); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("rule1", idExtractor.apply(result.get(0))); ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -324,8 +324,8 @@ private void testComposingBehaviorForRuleType( ); result = ruleGetter.apply(composing); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule2", idExtractor.apply(result.get(0))); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("rule2", idExtractor.apply(result.get(0))); ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -333,7 +333,7 @@ private void testComposingBehaviorForRuleType( ); result = ruleGetter.apply(composing); - Assert.assertTrue(result.isEmpty()); + Assertions.assertTrue(result.isEmpty()); } private void testComposingBehaviorForNonAdditiveRuleTypeWithInterval( @@ -354,8 +354,8 @@ private void testComposingBehaviorForNonAdditiveRuleTypeWithInterval( ); T result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertNotNull(result); - Assert.assertEquals("rule1", idExtractor.apply(result)); + Assertions.assertNotNull(result); + Assertions.assertEquals("rule1", idExtractor.apply(result)); ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -363,8 +363,8 @@ private void testComposingBehaviorForNonAdditiveRuleTypeWithInterval( ); result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertNotNull(result); - Assert.assertEquals("rule2", idExtractor.apply(result)); + Assertions.assertNotNull(result); + Assertions.assertEquals("rule2", idExtractor.apply(result)); ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -372,7 +372,7 @@ private void testComposingBehaviorForNonAdditiveRuleTypeWithInterval( ); result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertNull(result); + Assertions.assertNull(result); } /** @@ -399,8 +399,8 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( ); List result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule1", idExtractor.apply(result.get(0))); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("rule1", idExtractor.apply(result.get(0))); ReindexingRuleProvider emptyProvider = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -408,8 +408,8 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( ); result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertEquals(1, result.size()); - Assert.assertEquals("rule2", idExtractor.apply(result.get(0))); + Assertions.assertEquals(1, result.size()); + Assertions.assertEquals("rule2", idExtractor.apply(result.get(0))); ReindexingRuleProvider emptyProvider2 = InlineReindexingRuleProvider.builder().build(); composing = new ComposingReindexingRuleProvider( @@ -417,7 +417,7 @@ private void testComposingBehaviorForAdditiveRuleTypeWithInterval( ); result = ruleGetter.apply(composing, new IntervalAndTime(interval, REFERENCE_TIME)); - Assert.assertTrue(result.isEmpty()); + Assertions.assertTrue(result.isEmpty()); } /** diff --git a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java index 71dff44b9e6e..efd37311d33c 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/InlineReindexingRuleProviderTest.java @@ -33,8 +33,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import java.util.List; import java.util.function.BiFunction; @@ -66,15 +66,15 @@ public void test_constructor_nullListsDefaultToEmpty() null ); - Assert.assertNotNull(provider.getDeletionRules()); - Assert.assertTrue(provider.getDeletionRules().isEmpty()); - Assert.assertNotNull(provider.getIOConfigRules()); - Assert.assertTrue(provider.getIOConfigRules().isEmpty()); - Assert.assertNotNull(provider.getSegmentGranularityRules()); - Assert.assertTrue(provider.getSegmentGranularityRules().isEmpty()); - Assert.assertNotNull(provider.getTuningConfigRules()); - Assert.assertTrue(provider.getTuningConfigRules().isEmpty()); - Assert.assertTrue(provider.getDataSchemaRules().isEmpty()); + Assertions.assertNotNull(provider.getDeletionRules()); + Assertions.assertTrue(provider.getDeletionRules().isEmpty()); + Assertions.assertNotNull(provider.getIOConfigRules()); + Assertions.assertTrue(provider.getIOConfigRules().isEmpty()); + Assertions.assertNotNull(provider.getSegmentGranularityRules()); + Assertions.assertTrue(provider.getSegmentGranularityRules().isEmpty()); + Assertions.assertNotNull(provider.getTuningConfigRules()); + Assertions.assertTrue(provider.getTuningConfigRules().isEmpty()); + Assertions.assertTrue(provider.getDataSchemaRules().isEmpty()); } @Test @@ -89,17 +89,17 @@ public void test_reindexingRules_validateAdditivity() .build(); List noMatch = provider.getDeletionRules(INTERVAL_20_DAYS_OLD, REFERENCE_TIME); - Assert.assertTrue("No rules should match interval that's too recent", noMatch.isEmpty()); + Assertions.assertTrue(noMatch.isEmpty(), "No rules should match interval that's too recent"); List oneMatch = provider.getDeletionRules(INTERVAL_50_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("Only rule30d should match", 1, oneMatch.size()); - Assert.assertEquals("filter-30d", oneMatch.get(0).getId()); + Assertions.assertEquals(1, oneMatch.size(), "Only rule30d should match"); + Assertions.assertEquals("filter-30d", oneMatch.get(0).getId()); List multiMatch = provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals("All 3 additive rules should be returned", 3, multiMatch.size()); - Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-30d"))); - Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-60d"))); - Assert.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-90d"))); + Assertions.assertEquals(3, multiMatch.size(), "All 3 additive rules should be returned"); + Assertions.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-30d"))); + Assertions.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-60d"))); + Assertions.assertTrue(multiMatch.stream().anyMatch(r -> r.getId().equals("filter-90d"))); } @Test @@ -154,16 +154,16 @@ public void test_allRuleTypesWireCorrectly_withInterval() .dataSchemaRules(ImmutableList.of(dataSchemaRule)) .build(); - Assert.assertEquals(1, provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); - Assert.assertEquals("filter", provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); + Assertions.assertEquals(1, provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).size()); + Assertions.assertEquals("filter", provider.getDeletionRules(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).get(0).getId()); - Assert.assertEquals("ioconfig", provider.getIOConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + Assertions.assertEquals("ioconfig", provider.getIOConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("segmentGranularity", provider.getSegmentGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + Assertions.assertEquals("segmentGranularity", provider.getSegmentGranularityRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + Assertions.assertEquals("tuning", provider.getTuningConfigRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); - Assert.assertEquals("dataSchema", provider.getDataSchemaRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); + Assertions.assertEquals("dataSchema", provider.getDataSchemaRule(INTERVAL_100_DAYS_OLD, REFERENCE_TIME).getId()); } /** @@ -192,23 +192,23 @@ private void testNonAdditivity( builder = builderSetter.apply(builder, ImmutableList.of(rule30d, rule60d, rule90d)); InlineReindexingRuleProvider provider = builder.build(); - Assert.assertNull( - ruleTypeName + ": No rule should match interval that's too recent", - ruleGetter.apply(provider, INTERVAL_20_DAYS_OLD, REFERENCE_TIME) + Assertions.assertNull( + ruleGetter.apply(provider, INTERVAL_20_DAYS_OLD, REFERENCE_TIME), + ruleTypeName + ": No rule should match interval that's too recent" ); T oneMatch = ruleGetter.apply(provider, INTERVAL_50_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals( - ruleTypeName + ": Only 30d rule should match", + Assertions.assertEquals( ruleTypeName + "-30d", - oneMatch.getId() + oneMatch.getId(), + ruleTypeName + ": Only 30d rule should match" ); T multiMatch = ruleGetter.apply(provider, INTERVAL_100_DAYS_OLD, REFERENCE_TIME); - Assert.assertEquals( - ruleTypeName + ": Should return rule with oldest threshold (P90D)", + Assertions.assertEquals( ruleTypeName + "-90d", - multiMatch.getId() + multiMatch.getId(), + ruleTypeName + ": Should return rule with oldest threshold (P90D)" ); } @@ -222,7 +222,7 @@ private interface TriFunction public void test_getType_returnsInline() { InlineReindexingRuleProvider provider = InlineReindexingRuleProvider.builder().build(); - Assert.assertEquals("inline", provider.getType()); + Assertions.assertEquals("inline", provider.getType()); } private ReindexingDeletionRule createFilterRule(String id, Period period) From b55f7d8e8cd4a75d524f5da89462c677de730ce0 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 11:36:35 -0600 Subject: [PATCH 85/90] Add junit5 deps to indexing-service and migrate new test files to junit5 --- indexing-service/pom.xml | 15 ++ .../CascadingReindexingTemplateTest.java | 132 +++++++++--------- .../compact/ReindexingConfigBuilderTest.java | 102 +++++++------- .../ReindexingDeletionRuleOptimizerTest.java | 50 +++---- 4 files changed, 157 insertions(+), 142 deletions(-) diff --git a/indexing-service/pom.xml b/indexing-service/pom.xml index 3b21a710fb95..e3ba4108c3d1 100644 --- a/indexing-service/pom.xml +++ b/indexing-service/pom.xml @@ -198,6 +198,21 @@ junit test + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-migrationsupport + test + org.easymock easymock diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java index 14ebf40d5461..01b361004445 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/CascadingReindexingTemplateTest.java @@ -45,9 +45,9 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import javax.annotation.Nullable; import java.util.ArrayList; @@ -59,7 +59,7 @@ public class CascadingReindexingTemplateTest extends InitializedNullHandlingTest { private static final ObjectMapper OBJECT_MAPPER = new DefaultObjectMapper(); - @Before + @BeforeEach public void setUp() { OBJECT_MAPPER.registerModules(new SupervisorModule().getJacksonModules()); @@ -98,12 +98,12 @@ public void test_serde() throws Exception final String json = OBJECT_MAPPER.writeValueAsString(template); final CascadingReindexingTemplate fromJson = OBJECT_MAPPER.readValue(json, CascadingReindexingTemplate.class); - Assert.assertEquals(template.getDataSource(), fromJson.getDataSource()); - Assert.assertEquals(template.getTaskPriority(), fromJson.getTaskPriority()); - Assert.assertEquals(template.getInputSegmentSizeBytes(), fromJson.getInputSegmentSizeBytes()); - Assert.assertEquals(template.getEngine(), fromJson.getEngine()); - Assert.assertEquals(template.getTaskContext(), fromJson.getTaskContext()); - Assert.assertEquals(template.getType(), fromJson.getType()); + Assertions.assertEquals(template.getDataSource(), fromJson.getDataSource()); + Assertions.assertEquals(template.getTaskPriority(), fromJson.getTaskPriority()); + Assertions.assertEquals(template.getInputSegmentSizeBytes(), fromJson.getInputSegmentSizeBytes()); + Assertions.assertEquals(template.getEngine(), fromJson.getEngine()); + Assertions.assertEquals(template.getTaskContext(), fromJson.getTaskContext()); + Assertions.assertEquals(template.getType(), fromJson.getType()); } @Test @@ -134,15 +134,15 @@ public void test_serde_asDataSourceCompactionConfig() throws Exception final String json = OBJECT_MAPPER.writeValueAsString(template); final DataSourceCompactionConfig fromJson = OBJECT_MAPPER.readValue(json, DataSourceCompactionConfig.class); - Assert.assertTrue(fromJson instanceof CascadingReindexingTemplate); + Assertions.assertTrue(fromJson instanceof CascadingReindexingTemplate); final CascadingReindexingTemplate cascadingFromJson = (CascadingReindexingTemplate) fromJson; - Assert.assertEquals("testDataSource", cascadingFromJson.getDataSource()); - Assert.assertEquals(30, cascadingFromJson.getTaskPriority()); - Assert.assertEquals(500000L, cascadingFromJson.getInputSegmentSizeBytes()); - Assert.assertEquals(CompactionEngine.MSQ, cascadingFromJson.getEngine()); - Assert.assertEquals(ImmutableMap.of("key", "value"), cascadingFromJson.getTaskContext()); - Assert.assertEquals(CascadingReindexingTemplate.TYPE, cascadingFromJson.getType()); + Assertions.assertEquals("testDataSource", cascadingFromJson.getDataSource()); + Assertions.assertEquals(30, cascadingFromJson.getTaskPriority()); + Assertions.assertEquals(500000L, cascadingFromJson.getInputSegmentSizeBytes()); + Assertions.assertEquals(CompactionEngine.MSQ, cascadingFromJson.getEngine()); + Assertions.assertEquals(ImmutableMap.of("key", "value"), cascadingFromJson.getTaskContext()); + Assertions.assertEquals(CascadingReindexingTemplate.TYPE, cascadingFromJson.getType()); } @Test @@ -168,7 +168,7 @@ public void test_createCompactionJobs_ruleProviderNotReady() // Call createCompactionJobs - should return empty list without processing final List jobs = template.createCompactionJobs(null, null); - Assert.assertTrue(jobs.isEmpty()); + Assertions.assertTrue(jobs.isEmpty()); EasyMock.verify(notReadyProvider); } @@ -178,7 +178,7 @@ public void test_constructor_setBothSkipOffsetStrategiesThrowsException() final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.replay(mockProvider); - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> new CascadingReindexingTemplate( "testDataSource", @@ -193,7 +193,7 @@ public void test_constructor_setBothSkipOffsetStrategiesThrowsException() ) ); - Assert.assertEquals("Cannot set both skipOffsetFromNow and skipOffsetFromLatest", exception.getMessage()); + Assertions.assertEquals("Cannot set both skipOffsetFromNow and skipOffsetFromLatest", exception.getMessage()); EasyMock.verify(mockProvider); } @@ -203,7 +203,7 @@ public void test_constructor_nullDataSourceThrowsException() final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.replay(mockProvider); - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> new CascadingReindexingTemplate( null, // null dataSource @@ -218,14 +218,14 @@ public void test_constructor_nullDataSourceThrowsException() ) ); - Assert.assertTrue(exception.getMessage().contains("'dataSource' cannot be null")); + Assertions.assertTrue(exception.getMessage().contains("'dataSource' cannot be null")); EasyMock.verify(mockProvider); } @Test public void test_constructor_nullRuleProviderThrowsException() { - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> new CascadingReindexingTemplate( "testDataSource", @@ -240,7 +240,7 @@ public void test_constructor_nullRuleProviderThrowsException() ) ); - Assert.assertTrue(exception.getMessage().contains("'ruleProvider' cannot be null")); + Assertions.assertTrue(exception.getMessage().contains("'ruleProvider' cannot be null")); } @Test @@ -249,7 +249,7 @@ public void test_constructor_nullDefaultSegmentGranularityThrowsException() final ReindexingRuleProvider mockProvider = EasyMock.createMock(ReindexingRuleProvider.class); EasyMock.replay(mockProvider); - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> new CascadingReindexingTemplate( "testDataSource", @@ -264,7 +264,7 @@ public void test_constructor_nullDefaultSegmentGranularityThrowsException() ) ); - Assert.assertTrue(exception.getMessage().contains("'defaultSegmentGranularity' cannot be null")); + Assertions.assertTrue(exception.getMessage().contains("'defaultSegmentGranularity' cannot be null")); EasyMock.verify(mockProvider); } @@ -284,12 +284,12 @@ public void test_createCompactionJobs_simple() template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(2, processedIntervals.size()); + Assertions.assertEquals(2, processedIntervals.size()); // Intervals are now in chronological order (oldest first) - Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); - Assert.assertEquals(referenceTime.minusDays(7), processedIntervals.get(1).getEnd()); + Assertions.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(1).getStart()); + Assertions.assertEquals(referenceTime.minusDays(7), processedIntervals.get(1).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -309,8 +309,8 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_skipAllOfTime() List jobs = template.createCompactionJobs(mockSource, mockParams); - Assert.assertTrue(jobs.isEmpty()); - Assert.assertTrue(template.getProcessedIntervals().isEmpty()); + Assertions.assertTrue(jobs.isEmpty()); + Assertions.assertTrue(template.getProcessedIntervals().isEmpty()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -331,9 +331,9 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_skipsIntervalsExt template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assertions.assertEquals(1, processedIntervals.size()); + Assertions.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -354,9 +354,9 @@ public void test_createCompactionJobs_withSkipOffsetFromLatest_eliminatesInterva template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assertions.assertEquals(1, processedIntervals.size()); + Assertions.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -376,8 +376,8 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_skipAllOfTime() List jobs = template.createCompactionJobs(mockSource, mockParams); - Assert.assertTrue(jobs.isEmpty()); - Assert.assertTrue(template.getProcessedIntervals().isEmpty()); + Assertions.assertTrue(jobs.isEmpty()); + Assertions.assertTrue(template.getProcessedIntervals().isEmpty()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -398,9 +398,9 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_skipsIntervalsExtend template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assertions.assertEquals(1, processedIntervals.size()); + Assertions.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -421,9 +421,9 @@ public void test_createCompactionJobs_withSkipOffsetFromNow_eliminatesInterval() template.createCompactionJobs(mockSource, mockParams); List processedIntervals = template.getProcessedIntervals(); - Assert.assertEquals(1, processedIntervals.size()); - Assert.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); - Assert.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); + Assertions.assertEquals(1, processedIntervals.size()); + Assertions.assertEquals(DateTimes.MIN, processedIntervals.get(0).getStart()); + Assertions.assertEquals(referenceTime.minusDays(30), processedIntervals.get(0).getEnd()); EasyMock.verify(mockProvider, mockParams, mockSource); } @@ -504,7 +504,7 @@ public void test_generateAlignedSearchIntervals_withGranularityAlignment() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -615,7 +615,7 @@ public void test_generateAlignedSearchIntervals_withNonSegmentGranularityRuleSpl ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -696,7 +696,7 @@ public void test_generateAlignedSearchIntervals_withNoSegmentGranularityRules() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -797,7 +797,7 @@ public void test_generateAlignedSearchIntervals_prependIntervalForShortNonSegmen ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -908,7 +908,7 @@ public void test_generateAlignedSearchIntervals() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -944,12 +944,12 @@ public void test_generateAlignedSearchIntervals_noRulesThrowsException() Granularities.DAY ); - DruidException exception = Assert.assertThrows( + DruidException exception = Assertions.assertThrows( DruidException.class, () -> template.generateAlignedSearchIntervals(referenceTime) ); - Assert.assertTrue( + Assertions.assertTrue( exception.getMessage().contains("requires at least one reindexing rule") ); } @@ -1018,7 +1018,7 @@ public void test_generateAlignedSearchIntervals_splitPointSnapsToExistingBoundar ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1085,7 +1085,7 @@ public void test_generateAlignedSearchIntervals_prependAlignmentDoesNotExtendTim ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1161,7 +1161,7 @@ public void test_generateAlignedSearchIntervals_duplicateSplitPointsFiltered() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1220,7 +1220,7 @@ public void test_generateAlignedSearchIntervals_singleRuleOnly() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1284,7 +1284,7 @@ public void test_generateAlignedSearchIntervals_zeroPeriodRuleAppliesImmediately ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1379,7 +1379,7 @@ public void test_generateAlignedSearchIntervals_zeroPeriodRuleWithOtherRules() ); List actual = template.generateAlignedSearchIntervals(referenceTime); - Assert.assertEquals(expected, actual); + Assertions.assertEquals(expected, actual); } /** @@ -1433,15 +1433,15 @@ public void test_generateAlignedSearchIntervals_failsWhenDefaultGranularityIsCoa Granularities.MONTH // MONTH is coarser than HOUR! ); - IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, () -> template.generateAlignedSearchIntervals(referenceTime) ); - Assert.assertTrue( + Assertions.assertTrue( exception.getMessage().contains("Invalid segment granularity timeline") ); - Assert.assertTrue( + Assertions.assertTrue( exception.getMessage().contains("coarser granularity") ); } @@ -1494,15 +1494,15 @@ public void test_generateAlignedSearchIntervals_failsWhenOlderRuleHasFinerGranul Granularities.DAY ); - IllegalArgumentException exception = Assert.assertThrows( + IllegalArgumentException exception = Assertions.assertThrows( IllegalArgumentException.class, () -> template.generateAlignedSearchIntervals(referenceTime) ); - Assert.assertTrue( + Assertions.assertTrue( exception.getMessage().contains("Invalid segment granularity timeline") ); - Assert.assertTrue( + Assertions.assertTrue( exception.getMessage().contains("coarser granularity") ); } diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java index 396200e803ec..c9bc0c69f11d 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingConfigBuilderTest.java @@ -45,8 +45,8 @@ import org.joda.time.DateTime; import org.joda.time.Interval; import org.joda.time.Period; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; public class ReindexingConfigBuilderTest { @@ -90,16 +90,16 @@ public void test_applyTo_handlesSynteticSegmentGranularityInsertion() int count = configBuilder.applyTo(builder); - Assert.assertEquals(1, count); + Assertions.assertEquals(1, count); InlineSchemaDataSourceCompactionConfig config = builder.build(); - Assert.assertNotNull(config.getGranularitySpec()); - Assert.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); - Assert.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); - Assert.assertNotNull(config.getGranularitySpec().getQueryGranularity()); - Assert.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); - Assert.assertNotNull(config.getGranularitySpec().isRollup()); - Assert.assertTrue(config.getGranularitySpec().isRollup()); + Assertions.assertNotNull(config.getGranularitySpec()); + Assertions.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); + Assertions.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); + Assertions.assertNotNull(config.getGranularitySpec().getQueryGranularity()); + Assertions.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); + Assertions.assertNotNull(config.getGranularitySpec().isRollup()); + Assertions.assertTrue(config.getGranularitySpec().isRollup()); // Test applyToWithDetails() on a fresh builder InlineSchemaDataSourceCompactionConfig.Builder builderForDetails = @@ -109,16 +109,16 @@ public void test_applyTo_handlesSynteticSegmentGranularityInsertion() ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builderForDetails); // Verify count matches - Assert.assertEquals(count, buildResult.getRuleCount()); + Assertions.assertEquals(count, buildResult.getRuleCount()); // Verify applied rules - should only contain the data schema rule, not the synthetic segment granularity - Assert.assertNotNull(buildResult.getAppliedRules()); - Assert.assertEquals(1, buildResult.getAppliedRules().size()); - Assert.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingDataSchemaRule); + Assertions.assertNotNull(buildResult.getAppliedRules()); + Assertions.assertEquals(1, buildResult.getAppliedRules().size()); + Assertions.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingDataSchemaRule); // Verify config matches InlineSchemaDataSourceCompactionConfig configFromDetails = builderForDetails.build(); - Assert.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); + Assertions.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); } @Test @@ -151,37 +151,37 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount int count = configBuilder.applyTo(builder); - Assert.assertEquals(6, count); + Assertions.assertEquals(6, count); InlineSchemaDataSourceCompactionConfig config = builder.build(); - Assert.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); - Assert.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); + Assertions.assertNotNull(config.getGranularitySpec().getSegmentGranularity()); + Assertions.assertEquals(Granularities.DAY, config.getGranularitySpec().getSegmentGranularity()); - Assert.assertNotNull(config.getGranularitySpec().getQueryGranularity()); - Assert.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); - Assert.assertTrue(config.getGranularitySpec().isRollup()); + Assertions.assertNotNull(config.getGranularitySpec().getQueryGranularity()); + Assertions.assertEquals(Granularities.HOUR, config.getGranularitySpec().getQueryGranularity()); + Assertions.assertTrue(config.getGranularitySpec().isRollup()); - Assert.assertNotNull(config.getTuningConfig()); - Assert.assertNotNull(config.getMetricsSpec()); - Assert.assertEquals(1, config.getMetricsSpec().length); - Assert.assertEquals("count", config.getMetricsSpec()[0].getName()); + Assertions.assertNotNull(config.getTuningConfig()); + Assertions.assertNotNull(config.getMetricsSpec()); + Assertions.assertEquals(1, config.getMetricsSpec().length); + Assertions.assertEquals("count", config.getMetricsSpec()[0].getName()); - Assert.assertNotNull(config.getDimensionsSpec()); - Assert.assertNotNull(config.getIoConfig()); + Assertions.assertNotNull(config.getDimensionsSpec()); + Assertions.assertNotNull(config.getIoConfig()); - Assert.assertNotNull(config.getProjections()); - Assert.assertEquals(1, config.getProjections().size()); // only 1 as we match the 2nd dataSchemaRule + Assertions.assertNotNull(config.getProjections()); + Assertions.assertEquals(1, config.getProjections().size()); // only 1 as we match the 2nd dataSchemaRule - Assert.assertNotNull(config.getTransformSpec()); + Assertions.assertNotNull(config.getTransformSpec()); DimFilter appliedFilter = config.getTransformSpec().getFilter(); - Assert.assertTrue(appliedFilter instanceof NotDimFilter); + Assertions.assertTrue(appliedFilter instanceof NotDimFilter); NotDimFilter notFilter = (NotDimFilter) appliedFilter; - Assert.assertTrue(notFilter.getField() instanceof OrDimFilter); + Assertions.assertTrue(notFilter.getField() instanceof OrDimFilter); OrDimFilter orFilter = (OrDimFilter) notFilter.getField(); - Assert.assertEquals(2, orFilter.getFields().size()); // 2 filters combined + Assertions.assertEquals(2, orFilter.getFields().size()); // 2 filters combined // Now test applyToWithDetails() on a fresh builder InlineSchemaDataSourceCompactionConfig.Builder builderForDetails = @@ -191,24 +191,24 @@ public void test_applyTo_allRulesPresent_appliesAllConfigsAndReturnsCorrectCount ReindexingConfigBuilder.BuildResult buildResult = configBuilder.applyToWithDetails(builderForDetails); // Verify BuildResult count matches applyTo() count - Assert.assertEquals(count, buildResult.getRuleCount()); + Assertions.assertEquals(count, buildResult.getRuleCount()); // Verify applied rules list - Assert.assertNotNull(buildResult.getAppliedRules()); - Assert.assertEquals(6, buildResult.getAppliedRules().size()); + Assertions.assertNotNull(buildResult.getAppliedRules()); + Assertions.assertEquals(6, buildResult.getAppliedRules().size()); // Verify rule types in order: tuning, io, dataSchema, 2 deletion rules, segment granularity - Assert.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingTuningConfigRule); - Assert.assertTrue(buildResult.getAppliedRules().get(1) instanceof ReindexingIOConfigRule); - Assert.assertTrue(buildResult.getAppliedRules().get(2) instanceof ReindexingDataSchemaRule); - Assert.assertTrue(buildResult.getAppliedRules().get(3) instanceof ReindexingDeletionRule); - Assert.assertTrue(buildResult.getAppliedRules().get(4) instanceof ReindexingDeletionRule); - Assert.assertTrue(buildResult.getAppliedRules().get(5) instanceof ReindexingSegmentGranularityRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(0) instanceof ReindexingTuningConfigRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(1) instanceof ReindexingIOConfigRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(2) instanceof ReindexingDataSchemaRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(3) instanceof ReindexingDeletionRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(4) instanceof ReindexingDeletionRule); + Assertions.assertTrue(buildResult.getAppliedRules().get(5) instanceof ReindexingSegmentGranularityRule); // Verify the config produced by applyToWithDetails() matches the original InlineSchemaDataSourceCompactionConfig configFromDetails = builderForDetails.build(); - Assert.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); - Assert.assertEquals(config.getTuningConfig(), configFromDetails.getTuningConfig()); + Assertions.assertEquals(config.getGranularitySpec(), configFromDetails.getGranularitySpec()); + Assertions.assertEquals(config.getTuningConfig(), configFromDetails.getTuningConfig()); } @Test @@ -233,16 +233,16 @@ public void test_applyTo_noRulesPresent_appliesNothingAndReturnsZero() int count = configBuilder.applyTo(builder); - Assert.assertEquals(0, count); + Assertions.assertEquals(0, count); InlineSchemaDataSourceCompactionConfig config = builder.build(); - Assert.assertNull(config.getTuningConfig()); - Assert.assertNull(config.getMetricsSpec()); - Assert.assertNull(config.getDimensionsSpec()); - Assert.assertNull(config.getIoConfig()); - Assert.assertNull(config.getProjections()); - Assert.assertNull(config.getTransformSpec()); + Assertions.assertNull(config.getTuningConfig()); + Assertions.assertNull(config.getMetricsSpec()); + Assertions.assertNull(config.getDimensionsSpec()); + Assertions.assertNull(config.getIoConfig()); + Assertions.assertNull(config.getProjections()); + Assertions.assertNull(config.getTransformSpec()); } private ReindexingRuleProvider createFullyPopulatedProvider() diff --git a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java index 40fd6ca4a6dc..47b69031218a 100644 --- a/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java +++ b/indexing-service/src/test/java/org/apache/druid/indexing/compact/ReindexingDeletionRuleOptimizerTest.java @@ -47,9 +47,9 @@ import org.apache.druid.timeline.SegmentId; import org.easymock.EasyMock; import org.joda.time.DateTime; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import javax.annotation.Nullable; import java.util.ArrayList; @@ -71,7 +71,7 @@ public class ReindexingDeletionRuleOptimizerTest private IndexingStateFingerprintMapper fingerprintMapper; private ReindexingDeletionRuleOptimizer optimizer; - @Before + @BeforeEach public void setUp() { indexingStateStorage = new HeapMemoryIndexingStateStorage(); @@ -96,7 +96,7 @@ public void testOptimize_SingleFilter_NotOrFilter_NoFingerprints_ReturnsUnchange DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // No state for fp1, so config should be unchanged - Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); + Assertions.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } @Test @@ -115,7 +115,7 @@ public void testOptimize_SingleFilter_AlreadyApplied_RemovesTransformSpec() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // All filters optimized away, transform spec should be null - Assert.assertNull(result.getTransformSpec()); + Assertions.assertNull(result.getTransformSpec()); } @Test @@ -137,7 +137,7 @@ public void testOptimize_AllFiltersAlreadyApplied_RemovesTransformSpec() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // All filters already applied, transform spec should be removed - Assert.assertNull(result.getTransformSpec()); + Assertions.assertNull(result.getTransformSpec()); } @Test @@ -161,8 +161,8 @@ public void testOptimize_NoFiltersApplied_ReturnsAllExpected() // No filters were applied, so all should remain NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); - Assert.assertEquals(3, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); + Assertions.assertEquals(3, innerOr.getFields().size()); + Assertions.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterA, filterB, filterC))); } @Test @@ -192,7 +192,7 @@ public void testOptimize_PartiallyApplied_ReturnsDelta() Set resultSet = new HashSet<>(innerOr.getFields()); Set expectedSet = new HashSet<>(Arrays.asList(filterC, filterD)); - Assert.assertEquals(expectedSet, resultSet); + Assertions.assertEquals(expectedSet, resultSet); } @Test @@ -226,7 +226,7 @@ public void testOptimize_MultipleFingerprints_UnionOfMissing() Set resultSet = new HashSet<>(innerOr.getFields()); Set expectedSet = new HashSet<>(Arrays.asList(filterB, filterC, filterD)); - Assert.assertEquals(expectedSet, resultSet); + Assertions.assertEquals(expectedSet, resultSet); } @Test @@ -257,8 +257,8 @@ public void testOptimize_MultipleFingerprints_NoDuplicates() OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); Set resultSet = new HashSet<>(innerOr.getFields()); - Assert.assertEquals(2, resultSet.size()); - Assert.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); + Assertions.assertEquals(2, resultSet.size()); + Assertions.assertTrue(resultSet.containsAll(Arrays.asList(filterB, filterC))); } @Test @@ -279,7 +279,7 @@ public void testOptimize_MissingCompactionState_ReturnsAllFilters() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // No state available, all filters should remain - Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); + Assertions.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } @Test @@ -305,8 +305,8 @@ public void testOptimize_TransformSpecWithSingleFilter() // A was already applied, only B and C should remain NotDimFilter resultFilter = (NotDimFilter) result.getTransformSpec().getFilter(); OrDimFilter innerOr = (OrDimFilter) resultFilter.getField(); - Assert.assertEquals(2, innerOr.getFields().size()); - Assert.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); + Assertions.assertEquals(2, innerOr.getFields().size()); + Assertions.assertTrue(innerOr.getFields().containsAll(Arrays.asList(filterB, filterC))); } @Test @@ -327,7 +327,7 @@ public void testOptimize_SegmentsWithNoFingerprints() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // No fingerprints, all filters should remain - Assert.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); + Assertions.assertEquals(expectedFilter, result.getTransformSpec().getFilter()); } @@ -453,17 +453,17 @@ public void testOptimize_FilterVirtualColumns_SomeColumnsReferenced() // Filter remains, but vc2 should be filtered out VirtualColumns resultVCs = result.getTransformSpec().getVirtualColumns(); - Assert.assertNotNull(resultVCs); - Assert.assertEquals(2, resultVCs.getVirtualColumns().length); + Assertions.assertNotNull(resultVCs); + Assertions.assertEquals(2, resultVCs.getVirtualColumns().length); Set outputNames = new HashSet<>(); for (org.apache.druid.segment.VirtualColumn vc : resultVCs.getVirtualColumns()) { outputNames.add(vc.getOutputName()); } - Assert.assertTrue(outputNames.contains("vc1")); - Assert.assertFalse(outputNames.contains("vc2")); // vc2 should be filtered out - Assert.assertTrue(outputNames.contains("vc3")); + Assertions.assertTrue(outputNames.contains("vc1")); + Assertions.assertFalse(outputNames.contains("vc2")); // vc2 should be filtered out + Assertions.assertTrue(outputNames.contains("vc3")); } @Test @@ -488,7 +488,7 @@ public void testOptimize_FilterVirtualColumns_NoColumnsReferenced() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); - Assert.assertEquals(VirtualColumns.EMPTY, result.getTransformSpec().getVirtualColumns()); + Assertions.assertEquals(VirtualColumns.EMPTY, result.getTransformSpec().getVirtualColumns()); } @Test @@ -509,7 +509,7 @@ public void testOptimize_CandidateNeverCompacted_NoOptimization() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // Should return config unchanged since candidate was never compacted - Assert.assertSame(config, result); + Assertions.assertSame(config, result); } @Test @@ -526,7 +526,7 @@ public void testOptimize_NoTransformSpec_NoOptimization() DataSourceCompactionConfig result = optimizer.optimizeConfig(config, candidate, params); // Should return config unchanged - Assert.assertSame(config, result); + Assertions.assertSame(config, result); } /** From 889d8766bc1d8f2773eac99a441f23a131cd0f06 Mon Sep 17 00:00:00 2001 From: capistrant Date: Thu, 19 Feb 2026 14:53:40 -0600 Subject: [PATCH 86/90] Add vintage engine so junit4 and 5 can coexist in indexing-service --- indexing-service/pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indexing-service/pom.xml b/indexing-service/pom.xml index e3ba4108c3d1..3b2b06fc6123 100644 --- a/indexing-service/pom.xml +++ b/indexing-service/pom.xml @@ -213,6 +213,11 @@ junit-jupiter-migrationsupport test + + org.junit.vintage + junit-vintage-engine + test + org.easymock easymock From 8a14e85db1302fbff863e880e051f51cd99269f2 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 20 Feb 2026 10:38:46 -0600 Subject: [PATCH 87/90] Clean up ReindexingRule invalid input handling. Throw DruidException --- .../compaction/ReindexingDataSchemaRule.java | 5 +++++ .../compaction/ReindexingDeletionRule.java | 4 +++- .../compaction/ReindexingIOConfigRule.java | 4 +++- .../ReindexingTuningConfigRule.java | 4 +++- .../ReindexingDataSchemaRuleTest.java | 19 +++++++++++++++++++ .../ReindexingDeletionRuleTest.java | 5 +++-- .../ReindexingIOConfigRuleTest.java | 5 +++-- .../ReindexingTuningConfigRuleTest.java | 5 +++-- 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java index 5a9f94f01492..6ed678245ccf 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDataSchemaRule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import org.apache.druid.data.input.impl.AggregateProjectionSpec; +import org.apache.druid.error.InvalidInput; import org.apache.druid.java.util.common.granularity.Granularity; import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -68,6 +69,10 @@ public ReindexingDataSchemaRule( ) { super(id, description, olderThan); + InvalidInput.conditionalException( + (dimensionsSpec != null || metricsSpec != null || queryGranularity != null || rollup != null || projections != null), + "At least oe of 'dimensionsSpec', 'metricsSpec', 'queryGranularity', 'rollup' or 'projections' must be non-null" + ); this.dimensionsSpec = dimensionsSpec; this.metricsSpec = metricsSpec; this.queryGranularity = queryGranularity; diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java index 43465cc8bc52..a6b40aa40d62 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingDeletionRule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.error.InvalidInput; import org.apache.druid.query.filter.DimFilter; import org.apache.druid.segment.VirtualColumns; import org.joda.time.Period; @@ -97,7 +98,8 @@ public ReindexingDeletionRule( ) { super(id, description, olderThan); - this.deleteWhere = Objects.requireNonNull(deleteWhere, "deleteWhere cannot be null"); + InvalidInput.conditionalException(deleteWhere != null, "'deleteWhere' cannot be null"); + this.deleteWhere = deleteWhere; this.virtualColumns = virtualColumns; } diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java index 25f4af46ef20..1c34c269133a 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingIOConfigRule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.error.InvalidInput; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; import org.joda.time.Period; @@ -58,7 +59,8 @@ public ReindexingIOConfigRule( ) { super(id, description, olderThan); - this.ioConfig = Objects.requireNonNull(ioConfig, "ioConfig cannot be null"); + InvalidInput.conditionalException(ioConfig != null, "'ioConfig' cannot be null"); + this.ioConfig = ioConfig; } @JsonProperty diff --git a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java index 427643f768f7..e3bc98f3b0d8 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java +++ b/server/src/main/java/org/apache/druid/server/compaction/ReindexingTuningConfigRule.java @@ -21,6 +21,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.druid.error.InvalidInput; import org.apache.druid.segment.indexing.TuningConfig; import org.apache.druid.server.coordinator.UserCompactionTaskQueryTuningConfig; import org.joda.time.Period; @@ -67,7 +68,8 @@ public ReindexingTuningConfigRule( ) { super(id, description, olderThan); - this.tuningConfig = Objects.requireNonNull(tuningConfig, "tuningConfig cannot be null"); + InvalidInput.conditionalException(tuningConfig != null, "'tuningConfig' cannot be null"); + this.tuningConfig = tuningConfig; } @JsonProperty diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java index 8f48d8330b37..7afef2dce4d7 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDataSchemaRuleTest.java @@ -20,6 +20,7 @@ package org.apache.druid.server.compaction; import org.apache.druid.data.input.impl.LongDimensionSchema; +import org.apache.druid.error.DruidException; 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; @@ -348,4 +349,22 @@ public void test_constructor_emptyMetricsSpec_succeeds() Assertions.assertNotNull(rule.getMetricsSpec()); Assertions.assertEquals(0, rule.getMetricsSpec().length); } + + @Test + public void test_constructor_allNull_throwsDruidException() + { + Assertions.assertThrows( + DruidException.class, + () -> new ReindexingDataSchemaRule( + "test-id", + "description", + PERIOD_14_DAYS, + null, + null, + null, + null, + null + ) + ); + } } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java index becd915c9aa6..a99f3157527a 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingDeletionRuleTest.java @@ -20,6 +20,7 @@ package org.apache.druid.server.compaction; import com.google.common.collect.ImmutableList; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.math.expr.ExprMacroTable; @@ -189,10 +190,10 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() } @Test - public void test_constructor_nullDeleteWhere_throwsNullPointerException() + public void test_constructor_nullDeleteWhere_throwsDruidException() { Assertions.assertThrows( - NullPointerException.class, + DruidException.class, () -> new ReindexingDeletionRule("test-id", "description", PERIOD_30_DAYS, null, null) ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java index 472d8f9f1b5f..92ecd4d5f0b5 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingIOConfigRuleTest.java @@ -19,6 +19,7 @@ package org.apache.druid.server.compaction; +import org.apache.druid.error.DruidException; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; import org.apache.druid.server.coordinator.UserCompactionTaskIOConfig; @@ -161,10 +162,10 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() } @Test - public void test_constructor_nullIOConfig_throwsNullPointerException() + public void test_constructor_nullIOConfig_throwsDruidException() { Assertions.assertThrows( - NullPointerException.class, + DruidException.class, () -> new ReindexingIOConfigRule("test-id", "description", PERIOD_60_DAYS, null) ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java index 67e5d7c4b3a9..b3625c1e834b 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/ReindexingTuningConfigRuleTest.java @@ -19,6 +19,7 @@ package org.apache.druid.server.compaction; +import org.apache.druid.error.DruidException; import org.apache.druid.indexer.partitions.DynamicPartitionsSpec; import org.apache.druid.java.util.common.DateTimes; import org.apache.druid.java.util.common.Intervals; @@ -160,10 +161,10 @@ public void test_constructor_negativePeriod_throwsIllegalArgumentException() } @Test - public void test_constructor_nullTuningConfig_throwsNullPointerException() + public void test_constructor_nullTuningConfig_throwsDruidException() { Assertions.assertThrows( - NullPointerException.class, + DruidException.class, () -> new ReindexingTuningConfigRule("test-id", "description", PERIOD_21_DAYS, null) ); } From d3e89177f3ff41ce0c992299f9f4ddc9713c1368 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 20 Feb 2026 12:45:48 -0600 Subject: [PATCH 88/90] Fix the transform spec equality check in compactionstatus --- .../server/compaction/CompactionStatus.java | 15 ++- .../compaction/CompactionStatusTest.java | 97 +++++++++++++++++++ 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index be13ec40c4e9..7964685dd634 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -87,7 +87,7 @@ public enum State Evaluator::rollupIsUpToDate, Evaluator::dimensionsSpecIsUpToDate, Evaluator::metricsSpecIsUpToDate, - Evaluator::transformSpecFilterIsUpToDate, + Evaluator::transformSpecIsUpToDate, Evaluator::projectionsAreUpToDate ); @@ -567,9 +567,9 @@ private CompactionStatus metricsSpecIsUpToDate() return evaluateForAllCompactionStates(this::metricsSpecIsUpToDate); } - private CompactionStatus transformSpecFilterIsUpToDate() + private CompactionStatus transformSpecIsUpToDate() { - return evaluateForAllCompactionStates(this::transformSpecFilterIsUpToDate); + return evaluateForAllCompactionStates(this::transformSpecIsUpToDate); } private CompactionStatus partitionsSpecIsUpToDate(CompactionState lastCompactionState) @@ -751,17 +751,16 @@ private CompactionStatus metricsSpecIsUpToDate(CompactionState lastCompactionSta } } - private CompactionStatus transformSpecFilterIsUpToDate(CompactionState lastCompactionState) + private CompactionStatus transformSpecIsUpToDate(CompactionState lastCompactionState) { if (compactionConfig.getTransformSpec() == null) { return COMPLETE; } - CompactionTransformSpec existingTransformSpec = lastCompactionState.getTransformSpec(); return CompactionStatus.completeIfNullOrEqual( - "transformSpec filter", - compactionConfig.getTransformSpec().getFilter(), - existingTransformSpec == null ? null : existingTransformSpec.getFilter(), + "transformSpec", + compactionConfig.getTransformSpec(), + lastCompactionState.getTransformSpec(), String::valueOf ); } diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java index 2b274e805d4c..6b9fd815c6af 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java @@ -35,16 +35,22 @@ import org.apache.druid.java.util.common.Intervals; import org.apache.druid.java.util.common.granularity.Granularities; import org.apache.druid.java.util.common.granularity.Granularity; +import org.apache.druid.math.expr.ExprMacroTable; import org.apache.druid.query.aggregation.LongSumAggregatorFactory; +import org.apache.druid.query.filter.SelectorDimFilter; import org.apache.druid.segment.AutoTypeColumnSchema; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.TestDataSource; +import org.apache.druid.segment.VirtualColumns; +import org.apache.druid.segment.column.ColumnType; import org.apache.druid.segment.data.CompressionStrategy; import org.apache.druid.segment.metadata.DefaultIndexingStateFingerprintMapper; import org.apache.druid.segment.metadata.HeapMemoryIndexingStateStorage; import org.apache.druid.segment.metadata.IndexingStateCache; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; import org.apache.druid.segment.nested.NestedCommonFormatColumnFormatSpec; +import org.apache.druid.segment.transform.CompactionTransformSpec; +import org.apache.druid.segment.virtual.ExpressionVirtualColumn; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.InlineSchemaDataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskDimensionsConfig; @@ -474,6 +480,97 @@ public void testStatusWhenProjectionsMismatch() Assert.assertFalse(status.isComplete()); } + @Test + public void testStatusWhenTransformSpecVirtualColumnsMatch() + { + ExpressionVirtualColumn vc = new ExpressionVirtualColumn( + "extractedField", "concat(metadata, '_category')", ColumnType.STRING, ExprMacroTable.nil() + ); + CompactionTransformSpec transformSpec = new CompactionTransformSpec( + new SelectorDimFilter("extractedField", "foo", null), + VirtualColumns.create(vc) + ); + CompactionState lastCompactionState = new CompactionState( + null, null, null, transformSpec, IndexSpec.getDefault(), null, null + ); + DataSourceCompactionConfig compactionConfig = InlineSchemaDataSourceCompactionConfig + .builder() + .forDataSource(TestDataSource.WIKI) + .withTransformSpec(transformSpec) + .build(); + + DataSegment segment = DataSegment.builder(WIKI_SEGMENT).lastCompactionState(lastCompactionState).build(); + CompactionStatus status = CompactionStatus.compute( + CompactionCandidate.from(List.of(segment), null), compactionConfig, fingerprintMapper + ); + Assert.assertTrue(status.isComplete()); + } + + @Test + public void testStatusWhenTransformSpecVirtualColumnsMismatch() + { + SelectorDimFilter filter = new SelectorDimFilter("extractedField", "foo", null); + ExpressionVirtualColumn oldVc = new ExpressionVirtualColumn( + "extractedField", "concat(metadata, '_old')", ColumnType.STRING, ExprMacroTable.nil() + ); + ExpressionVirtualColumn newVc = new ExpressionVirtualColumn( + "extractedField", "concat(metadata, '_new')", ColumnType.STRING, ExprMacroTable.nil() + ); + + CompactionState lastCompactionState = new CompactionState( + null, null, null, + new CompactionTransformSpec(filter, VirtualColumns.create(oldVc)), + IndexSpec.getDefault(), null, null + ); + DataSourceCompactionConfig compactionConfig = InlineSchemaDataSourceCompactionConfig + .builder() + .forDataSource(TestDataSource.WIKI) + .withTransformSpec(new CompactionTransformSpec(filter, VirtualColumns.create(newVc))) + .build(); + + DataSegment segment = DataSegment.builder(WIKI_SEGMENT).lastCompactionState(lastCompactionState).build(); + CompactionStatus status = CompactionStatus.compute( + CompactionCandidate.from(List.of(segment), null), compactionConfig, fingerprintMapper + ); + Assert.assertFalse(status.isComplete()); + Assert.assertTrue(status.getReason().startsWith("'transformSpec' mismatch")); + } + + @Test + public void test_evaluate_needsCompactionWhenMismatchedFingerprintStateHasDifferentVirtualColumns() + { + SelectorDimFilter filter = new SelectorDimFilter("extractedField", "foo", null); + ExpressionVirtualColumn vc = new ExpressionVirtualColumn( + "extractedField", "concat(metadata, '_category')", ColumnType.STRING, ExprMacroTable.nil() + ); + + DataSourceCompactionConfig oldConfig = InlineSchemaDataSourceCompactionConfig + .builder() + .forDataSource(TestDataSource.WIKI) + .withTransformSpec(new CompactionTransformSpec(filter, null)) + .build(); + CompactionState oldState = oldConfig.toCompactionState(); + String oldFingerprint = fingerprintMapper.generateFingerprint(TestDataSource.WIKI, oldState); + + DataSourceCompactionConfig newConfig = InlineSchemaDataSourceCompactionConfig + .builder() + .forDataSource(TestDataSource.WIKI) + .withTransformSpec(new CompactionTransformSpec(filter, VirtualColumns.create(vc))) + .build(); + + indexingStateStorage.upsertIndexingState(TestDataSource.WIKI, oldFingerprint, oldState, DateTimes.nowUtc()); + syncCacheFromManager(); + + List segments = List.of( + DataSegment.builder(WIKI_SEGMENT).indexingStateFingerprint(oldFingerprint).build() + ); + CompactionStatus status = CompactionStatus.compute( + CompactionCandidate.from(segments, null), newConfig, fingerprintMapper + ); + Assert.assertFalse(status.isComplete()); + Assert.assertTrue(status.getReason().startsWith("'transformSpec' mismatch")); + } + @Test public void testStatusWhenAutoSchemaMatch() { From ede35968a2a31203f7e6fae9245c9d6f44ed172f Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 20 Feb 2026 13:01:14 -0600 Subject: [PATCH 89/90] Fixup checkstyle --- .../server/compaction/CompactionStatus.java | 1 - .../server/compaction/CompactionStatusTest.java | 16 +++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 7964685dd634..1ab49edb8c94 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -34,7 +34,6 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; -import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.timeline.CompactionState; diff --git a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java index 6b9fd815c6af..370dcaf444d5 100644 --- a/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java +++ b/server/src/test/java/org/apache/druid/server/compaction/CompactionStatusTest.java @@ -491,7 +491,13 @@ public void testStatusWhenTransformSpecVirtualColumnsMatch() VirtualColumns.create(vc) ); CompactionState lastCompactionState = new CompactionState( - null, null, null, transformSpec, IndexSpec.getDefault(), null, null + null, + null, + null, + transformSpec, + IndexSpec.getDefault(), + null, + null ); DataSourceCompactionConfig compactionConfig = InlineSchemaDataSourceCompactionConfig .builder() @@ -518,9 +524,13 @@ public void testStatusWhenTransformSpecVirtualColumnsMismatch() ); CompactionState lastCompactionState = new CompactionState( - null, null, null, + null, + null, + null, new CompactionTransformSpec(filter, VirtualColumns.create(oldVc)), - IndexSpec.getDefault(), null, null + IndexSpec.getDefault(), + null, + null ); DataSourceCompactionConfig compactionConfig = InlineSchemaDataSourceCompactionConfig .builder() From 54a4b6a9a621c054a679f4f5072edd1304aabcd6 Mon Sep 17 00:00:00 2001 From: capistrant Date: Fri, 20 Feb 2026 14:29:30 -0600 Subject: [PATCH 90/90] fix regression now that our check on transform spec is more involved --- .../druid/server/compaction/CompactionStatus.java | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java index 1ab49edb8c94..10076a719241 100644 --- a/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java +++ b/server/src/main/java/org/apache/druid/server/compaction/CompactionStatus.java @@ -34,6 +34,7 @@ import org.apache.druid.query.aggregation.AggregatorFactory; import org.apache.druid.segment.IndexSpec; import org.apache.druid.segment.metadata.IndexingStateFingerprintMapper; +import org.apache.druid.segment.transform.CompactionTransformSpec; import org.apache.druid.server.coordinator.DataSourceCompactionConfig; import org.apache.druid.server.coordinator.UserCompactionTaskGranularityConfig; import org.apache.druid.timeline.CompactionState; @@ -752,14 +753,20 @@ private CompactionStatus metricsSpecIsUpToDate(CompactionState lastCompactionSta private CompactionStatus transformSpecIsUpToDate(CompactionState lastCompactionState) { - if (compactionConfig.getTransformSpec() == null) { + final CompactionTransformSpec configuredSpec = compactionConfig.getTransformSpec(); + if (configuredSpec == null + || (configuredSpec.getFilter() == null && configuredSpec.getVirtualColumns().isEmpty())) { return COMPLETE; } + final CompactionTransformSpec existingSpec = Configs.valueOrDefault( + lastCompactionState.getTransformSpec(), + new CompactionTransformSpec(null, null) + ); return CompactionStatus.completeIfNullOrEqual( "transformSpec", - compactionConfig.getTransformSpec(), - lastCompactionState.getTransformSpec(), + configuredSpec, + existingSpec, String::valueOf ); }