kfaraz commented on code in PR #20063:
URL: https://github.com/apache/druid/pull/20063#discussion_r3812675906
##########
server/src/main/java/org/apache/druid/metadata/MetadataRuleManager.java:
##########
@@ -41,6 +42,14 @@ public interface MetadataRuleManager
List<Rule> getRulesWithDefault(String dataSource);
+ /**
+ * Immutable snapshot of the rules of all datasources. A caller that needs
the rules
+ * of more than one datasource, or of the same datasource more than once,
must use a
+ * snapshot so that a concurrent rule update cannot change the answer part
way
+ * through.
Review Comment:
Slightly reworded to avoid confusion since the current comment somewhat
gives the idea that subsequent calls to this method would return the same
results.
```suggestion
* Current snapshot of the rules of all datasources. Using a single
snapshot while performing an
* operation (such as a Coordinator duty run) allows the steps within the
operation to remain
* consistent with each other, even if the rules are updated concurrently.
```
##########
server/src/main/java/org/apache/druid/server/coordinator/duty/UnloadUnusedSegments.java:
##########
@@ -83,13 +80,14 @@ private int dropUnusedSegments(
ServerHolder serverHolder,
DruidCoordinatorRuntimeParams params,
CoordinatorRunStats stats,
- Map<String, Boolean> broadcastStatusByDatasource
+ Map<String, Boolean> broadcastStatusByDatasource,
+ RetentionRulesSnapshot rulesSnapshot
Review Comment:
Nit: we can skip the extra arg.
##########
server/src/main/java/org/apache/druid/metadata/MetadataRuleManager.java:
##########
@@ -41,6 +42,14 @@ public interface MetadataRuleManager
List<Rule> getRulesWithDefault(String dataSource);
Review Comment:
I feel like we should get rid of these methods now since the new method is
superior to all of these. We should just expose these methods on the snapshot
class now.
##########
server/src/main/java/org/apache/druid/server/coordinator/rules/RetentionRulesSnapshot.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.coordinator.rules;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable snapshot of the retention rules of every datasource, taken once
at the
+ * start of a coordinator run and carried in
+ * {@link org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams}.
+ * <p>
+ * Duties must read rules from this snapshot rather than from
+ * {@link org.apache.druid.metadata.MetadataRuleManager} directly. That
manager swaps
+ * its entire rule map as soon as a rule update is submitted, so a duty
reading it
+ * per segment can apply the old rules to some segments and the new rules to
others
+ * within a single run. The new rules are then evaluated against values the
run has
+ * already snapshotted from {@link
org.apache.druid.server.coordinator.CoordinatorDynamicConfig},
+ * notably {@code historicalTierAliases}: a rule naming a virtual tier is
meaningful
Review Comment:
```suggestion
* e.g. {@code historicalTierAliases}: a rule naming a virtual tier is
meaningful
```
##########
server/src/main/java/org/apache/druid/server/coordinator/rules/RetentionRulesSnapshot.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.coordinator.rules;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable snapshot of the retention rules of every datasource, taken once
at the
+ * start of a coordinator run and carried in
+ * {@link org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams}.
+ * <p>
+ * Duties must read rules from this snapshot rather than from
+ * {@link org.apache.druid.metadata.MetadataRuleManager} directly. That
manager swaps
+ * its entire rule map as soon as a rule update is submitted, so a duty
reading it
+ * per segment can apply the old rules to some segments and the new rules to
others
+ * within a single run. The new rules are then evaluated against values the
run has
+ * already snapshotted from {@link
org.apache.druid.server.coordinator.CoordinatorDynamicConfig},
Review Comment:
simplified a bit
```suggestion
* Duties within a run must use a single snapshot to remain consistent with
each other.
```
##########
server/src/test/java/org/apache/druid/server/coordinator/duty/RunRulesTest.java:
##########
@@ -1286,6 +1209,94 @@ public void
testSegmentWithZeroRequiredReplicasHasZeroReplicationFactor()
Assert.assertEquals(0, replicaCounts.requiredAndLoadable());
}
+ @Test
+ public void testRuleChangeDuringRunIsNotObservedInThatRun()
+ {
+ mockPeon.loadSegment(EasyMock.anyObject(), EasyMock.anyObject(),
EasyMock.anyObject());
+ EasyMock.expectLastCall().atLeastOnce();
+ mockEmptyPeon();
+
+ final TestMetadataRuleManager ruleManager = new TestMetadataRuleManager();
+ ruleManager.overrideRule(
+ DATASOURCE,
+ Collections.singletonList(new
ForeverLoadRule(ImmutableMap.of("normal", 1), null)),
+ AUDIT_INFO
+ );
+
+ final DruidCluster druidCluster = DruidCluster
+ .builder()
+ .addTier("normal", createServerHolder("server1", "normal", mockPeon))
+ .build();
+
+ final DruidCoordinatorRuntimeParams params =
createCoordinatorRuntimeParams(druidCluster)
+ .withRetentionRulesSnapshot(ruleManager.getRulesSnapshot())
+ .withBalancerStrategy(new CostBalancerStrategy(balancerExecutor))
+ .withDynamicConfigs(
+
CoordinatorDynamicConfig.builder().withSmartSegmentLoading(false).build()
+ )
+ .withSegmentAssignerUsing(loadQueueManager)
+ .build();
+
+ // The rules change after the snapshot was taken, but before the duty runs
+ ruleManager.overrideRule(DATASOURCE, Collections.singletonList(new
ForeverDropRule()), AUDIT_INFO);
Review Comment:
I feel like this test is a bit of an overkill.
Since we are explicitly passing a snapshot into the runtime params, it seems
unnecessary to verify that performing operations on the rule manager directly
would not affect the duties (as the duties have no way to access the rule
manager itself).
The tests in `SQLMetadataRuleManagerTest` should suffice.
##########
server/src/main/java/org/apache/druid/server/coordinator/rules/RetentionRulesSnapshot.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.coordinator.rules;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable snapshot of the retention rules of every datasource, taken once
at the
+ * start of a coordinator run and carried in
+ * {@link org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams}.
+ * <p>
+ * Duties must read rules from this snapshot rather than from
+ * {@link org.apache.druid.metadata.MetadataRuleManager} directly. That
manager swaps
+ * its entire rule map as soon as a rule update is submitted, so a duty
reading it
+ * per segment can apply the old rules to some segments and the new rules to
others
+ * within a single run. The new rules are then evaluated against values the
run has
+ * already snapshotted from {@link
org.apache.druid.server.coordinator.CoordinatorDynamicConfig},
+ * notably {@code historicalTierAliases}: a rule naming a virtual tier is
meaningful
+ * only together with the alias config that resolves it, and a rule resolving
to no
+ * tier at all makes every existing replica look unwanted.
+ */
+public class RetentionRulesSnapshot
+{
+ private static final RetentionRulesSnapshot EMPTY = new
RetentionRulesSnapshot(Map.of(), List.of());
+
+ /**
+ * Rules of each datasource, already concatenated with {@link
#clusterDefaultRules}.
+ */
+ private final Map<String, List<Rule>> datasourceToRulesWithDefault;
+ private final List<Rule> clusterDefaultRules;
+
+ public static RetentionRulesSnapshot empty()
+ {
+ return EMPTY;
+ }
+
+ /**
+ * @param datasourceToRules Rules configured for each datasource, not
including the
+ * cluster defaults.
+ * @param clusterDefaultRules Rules configured for the default datasource.
These apply
+ * to every datasource after its own rules.
+ */
+ public RetentionRulesSnapshot(Map<String, List<Rule>> datasourceToRules,
List<Rule> clusterDefaultRules)
+ {
+ this.clusterDefaultRules = List.copyOf(clusterDefaultRules);
+
+ final Map<String, List<Rule>> rulesWithDefault =
Maps.newHashMapWithExpectedSize(datasourceToRules.size());
+ datasourceToRules.forEach((datasource, rules) -> {
+ final List<Rule> combined = new ArrayList<>(rules.size() +
this.clusterDefaultRules.size());
+ combined.addAll(rules);
+ combined.addAll(this.clusterDefaultRules);
+ rulesWithDefault.put(datasource, Collections.unmodifiableList(combined));
+ });
+ this.datasourceToRulesWithDefault = Map.copyOf(rulesWithDefault);
+ }
+
+ /**
+ * Rules of the given datasource followed by the cluster default rules, or
just the
+ * cluster defaults if the datasource has no rules of its own.
Review Comment:
```suggestion
* All retention rules applicable to segments of this datasource.
* The returned list contains the override rules specified for the
datasource followed by the cluster default rules.
```
##########
server/src/test/java/org/apache/druid/server/coordinator/duty/UnloadUnusedSegmentsTest.java:
##########
@@ -274,6 +280,36 @@ public void test_unloadUnusedSegmentsFromAllServers()
Assert.assertEquals(1L, stats.getSegmentStat(Stats.Segments.UNNEEDED,
"tier2", broadcastDatasource));
}
+ @Test
+ public void test_broadcastStatusComesFromRulesSnapshot()
Review Comment:
```suggestion
public void test_broadcastSegmentsAreDropped_ifSnapshotHasNoBroadcastRule()
```
##########
server/src/main/java/org/apache/druid/server/coordinator/rules/RetentionRulesSnapshot.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.coordinator.rules;
+
+import com.google.common.collect.Maps;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Immutable snapshot of the retention rules of every datasource, taken once
at the
+ * start of a coordinator run and carried in
+ * {@link org.apache.druid.server.coordinator.DruidCoordinatorRuntimeParams}.
+ * <p>
+ * Duties must read rules from this snapshot rather than from
+ * {@link org.apache.druid.metadata.MetadataRuleManager} directly. That
manager swaps
+ * its entire rule map as soon as a rule update is submitted, so a duty
reading it
+ * per segment can apply the old rules to some segments and the new rules to
others
+ * within a single run. The new rules are then evaluated against values the
run has
+ * already snapshotted from {@link
org.apache.druid.server.coordinator.CoordinatorDynamicConfig},
+ * notably {@code historicalTierAliases}: a rule naming a virtual tier is
meaningful
+ * only together with the alias config that resolves it, and a rule resolving
to no
+ * tier at all makes every existing replica look unwanted.
+ */
+public class RetentionRulesSnapshot
+{
+ private static final RetentionRulesSnapshot EMPTY = new
RetentionRulesSnapshot(Map.of(), List.of());
+
+ /**
+ * Rules of each datasource, already concatenated with {@link
#clusterDefaultRules}.
+ */
+ private final Map<String, List<Rule>> datasourceToRulesWithDefault;
+ private final List<Rule> clusterDefaultRules;
+
+ public static RetentionRulesSnapshot empty()
+ {
+ return EMPTY;
+ }
+
+ /**
+ * @param datasourceToRules Rules configured for each datasource, not
including the
+ * cluster defaults.
+ * @param clusterDefaultRules Rules configured for the default datasource.
These apply
+ * to every datasource after its own rules.
+ */
+ public RetentionRulesSnapshot(Map<String, List<Rule>> datasourceToRules,
List<Rule> clusterDefaultRules)
+ {
+ this.clusterDefaultRules = List.copyOf(clusterDefaultRules);
+
+ final Map<String, List<Rule>> rulesWithDefault =
Maps.newHashMapWithExpectedSize(datasourceToRules.size());
+ datasourceToRules.forEach((datasource, rules) -> {
+ final List<Rule> combined = new ArrayList<>(rules.size() +
this.clusterDefaultRules.size());
+ combined.addAll(rules);
+ combined.addAll(this.clusterDefaultRules);
+ rulesWithDefault.put(datasource, Collections.unmodifiableList(combined));
+ });
+ this.datasourceToRulesWithDefault = Map.copyOf(rulesWithDefault);
+ }
+
+ /**
+ * Rules of the given datasource followed by the cluster default rules, or
just the
+ * cluster defaults if the datasource has no rules of its own.
+ */
+ public List<Rule> getRulesWithDefault(String datasource)
Review Comment:
Should we rename this method to reduce ambiguity?
Maybe something like
```suggestion
public List<Rule> getAllRulesApplicableOn(String datasource)
```
##########
server/src/test/java/org/apache/druid/server/coordinator/duty/RunRulesTest.java:
##########
@@ -1286,6 +1209,94 @@ public void
testSegmentWithZeroRequiredReplicasHasZeroReplicationFactor()
Assert.assertEquals(0, replicaCounts.requiredAndLoadable());
}
+ @Test
+ public void testRuleChangeDuringRunIsNotObservedInThatRun()
+ {
+ mockPeon.loadSegment(EasyMock.anyObject(), EasyMock.anyObject(),
EasyMock.anyObject());
+ EasyMock.expectLastCall().atLeastOnce();
+ mockEmptyPeon();
+
+ final TestMetadataRuleManager ruleManager = new TestMetadataRuleManager();
+ ruleManager.overrideRule(
+ DATASOURCE,
+ Collections.singletonList(new
ForeverLoadRule(ImmutableMap.of("normal", 1), null)),
+ AUDIT_INFO
+ );
+
+ final DruidCluster druidCluster = DruidCluster
+ .builder()
+ .addTier("normal", createServerHolder("server1", "normal", mockPeon))
+ .build();
+
+ final DruidCoordinatorRuntimeParams params =
createCoordinatorRuntimeParams(druidCluster)
+ .withRetentionRulesSnapshot(ruleManager.getRulesSnapshot())
+ .withBalancerStrategy(new CostBalancerStrategy(balancerExecutor))
+ .withDynamicConfigs(
+
CoordinatorDynamicConfig.builder().withSmartSegmentLoading(false).build()
+ )
+ .withSegmentAssignerUsing(loadQueueManager)
+ .build();
+
+ // The rules change after the snapshot was taken, but before the duty runs
+ ruleManager.overrideRule(DATASOURCE, Collections.singletonList(new
ForeverDropRule()), AUDIT_INFO);
+
+ final CoordinatorRunStats stats = runDutyAndGetStats(params);
+
+ // The snapshotted load rule is applied, not the drop rule that replaced it
+ Assert.assertEquals(24L, stats.getSegmentStat(Stats.Segments.ASSIGNED,
"normal", DATASOURCE));
+ Assert.assertEquals(0L, stats.get(Stats.Segments.DELETED,
DATASOURCE_STAT_KEY));
+ }
+
+ @Test
+ public void testRuleChangeToUnresolvedAliasTierDoesNotDropReplicas()
Review Comment:
Is there also a test that verifies that unresolved alias tier does drop
replicas?
I feel like that would add more value than verifying that the snapshot does
not change.
##########
server/src/main/java/org/apache/druid/server/coordinator/duty/RunRules.java:
##########
@@ -159,11 +156,14 @@ private void
alertForInvalidRules(StrategicSegmentAssigner segmentAssigner)
);
}
- private Set<String> getBroadcastDatasources(DruidCoordinatorRuntimeParams
params)
+ private Set<String> getBroadcastDatasources(
+ DruidCoordinatorRuntimeParams params,
+ RetentionRulesSnapshot rulesSnapshot
Review Comment:
Nit: We don't need this as a separate arg. It can be extracted from the
params.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]