>From Preetham Poluparthi <[email protected]>: Preetham Poluparthi has submitted this change. ( https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20282?usp=email )
Change subject: [ASTERIXDB-3632] Fix NPEs in Index Advisor and add error handling for missing samples ...................................................................... [ASTERIXDB-3632] Fix NPEs in Index Advisor and add error handling for missing samples - user model changes: no - storage format changes: no - interface changes: no Ext-ref: MB-66492 Details: This patch addresses several corner cases in the Index Advisor: - Fixes null pointer exceptions that occur in scenarios such as: - Collections with secondary primary indexes - Cases where CBO bails out - Adds proper error handling when an `ADVISE` query is issued without CBO enabled - Introduces error reporting for collections that have no samples, suggesting the use of ANALYSE SAMPLE before issuing advise Change-Id: I346728dd634934416d7115a06ab15572b5a98854 Reviewed-on: https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20282 Reviewed-by: Ali Alsuliman <[email protected]> Integration-Tests: Jenkins <[email protected]> Reviewed-by: Preetham Poluparthi <[email protected]> Tested-by: Preetham Poluparthi <[email protected]> --- M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/EnumerateJoinsRule.java M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdviseIndexRule.java M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdvisorConditionParser.java M asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/FakeIndexProvider.java M asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java M hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/base/IndexAdvisor.java M hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/exceptions/ErrorCode.java M hyracks-fullstack/hyracks/hyracks-api/src/main/resources/errormsg/en.properties 10 files changed, 116 insertions(+), 42 deletions(-) Approvals: Ali Alsuliman: Looks good to me, approved Preetham Poluparthi: Looks good to me, but someone else must approve; Verified Jenkins: Verified diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java index 5bc1e90..09cf843 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceJoinAccessMethodRule.java @@ -123,7 +123,7 @@ throws AlgebricksException { clear(); boolean adviseIndex = context.getIndexAdvisor().getAdvise(); - if (adviseIndex) { + if (adviseIndex && context.getIndexAdvisor().getFakeIndexProvider() != null) { setMetadataIndexDeclarations(context, (IIndexProvider) context.getIndexAdvisor().getFakeIndexProvider()); } else { setMetadataIndexDeclarations(context, (IIndexProvider) context.getMetadataProvider()); diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java index 56ee034..df9fb2c 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/am/IntroduceSelectAccessMethodRule.java @@ -138,7 +138,7 @@ clear(); boolean adviseIndex = context.getIndexAdvisor().getAdvise(); - if (adviseIndex) { + if (adviseIndex && context.getIndexAdvisor().getFakeIndexProvider() != null) { setMetadataIndexDeclarations(context, (IIndexProvider) context.getIndexAdvisor().getFakeIndexProvider()); } else { setMetadataIndexDeclarations(context, (IIndexProvider) context.getMetadataProvider()); diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/EnumerateJoinsRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/EnumerateJoinsRule.java index 6e4208d..b02301f 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/EnumerateJoinsRule.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/EnumerateJoinsRule.java @@ -28,6 +28,8 @@ import org.apache.asterix.common.annotations.IndexedNLJoinExpressionAnnotation; import org.apache.asterix.common.annotations.SkipSecondaryIndexSearchExpressionAnnotation; +import org.apache.asterix.common.metadata.DatasetFullyQualifiedName; +import org.apache.asterix.metadata.declared.DatasetDataSource; import org.apache.asterix.metadata.declared.IIndexProvider; import org.apache.asterix.metadata.entities.Index; import org.apache.asterix.optimizer.rules.cbo.indexadvisor.AdvisorPlanParser; @@ -71,6 +73,7 @@ import org.apache.hyracks.algebricks.core.algebra.util.OperatorManipulationUtil; import org.apache.hyracks.algebricks.core.rewriter.base.IAlgebraicRewriteRule; import org.apache.hyracks.algebricks.core.rewriter.base.PhysicalOptimizationConfig; +import org.apache.hyracks.api.exceptions.ErrorCode; import org.apache.hyracks.api.exceptions.IWarningCollector; import org.apache.hyracks.api.exceptions.Warning; import org.apache.logging.log4j.LogManager; @@ -140,8 +143,8 @@ public boolean rewritePre(Mutable<ILogicalOperator> opRef, IOptimizationContext context) throws AlgebricksException { - boolean cboMode = this.getCBOMode(context); - boolean cboTestMode = this.getCBOTestMode(context); + boolean cboMode = getCBOMode(context); + boolean cboTestMode = getCBOTestMode(context); if (!(cboMode || cboTestMode)) { return false; @@ -215,6 +218,9 @@ joinEnum.stats = new Stats(context, joinEnum); if (cboMode) { if (!doAllDataSourcesHaveSamples(leafInputs, context)) { + if (adviseIndex) { + errorOutIndexAdvisorSamplesNotFound(leafInputs, context); + } return cleanUp(); } } @@ -278,6 +284,9 @@ fakeLeafInputsMap, context, indexProvider); if (cboMode) { if (!doAllDataSourcesHaveSamples(leafInputs, context)) { + if (adviseIndex) { + errorOutIndexAdvisorSamplesNotFound(leafInputs, context); + } return cleanUp(); } } @@ -768,12 +777,12 @@ } } - private boolean getCBOMode(IOptimizationContext context) { + public static boolean getCBOMode(IOptimizationContext context) { PhysicalOptimizationConfig physOptConfig = context.getPhysicalOptimizationConfig(); return physOptConfig.getCBOMode(); } - private boolean getCBOTestMode(IOptimizationContext context) { + public static boolean getCBOTestMode(IOptimizationContext context) { PhysicalOptimizationConfig physOptConfig = context.getPhysicalOptimizationConfig(); return physOptConfig.getCBOTestMode(); } @@ -1521,4 +1530,37 @@ } return (leafInputs.size() == n); } + + private void errorOutIndexAdvisorSamplesNotFound(List<ILogicalOperator> leafInputs, IOptimizationContext context) + throws AlgebricksException { + for (ILogicalOperator li : leafInputs) { + DataSourceScanOperator scanOp = joinEnum.findDataSourceScanOperator(li); + if (scanOp == null) { + // Scan Operator not found + continue; + } + Stats handle = joinEnum.getStatsHandle(); + if (handle == null) { + continue; + } + Index index = handle.findSampleIndex(scanOp, context); + if (index == null) { + errorOutIndexAdvisorSampleNotFound(scanOp, context); + } + } + } + + private void errorOutIndexAdvisorSampleNotFound(DataSourceScanOperator scanOperator, IOptimizationContext context) throws AlgebricksException { + if (!(scanOperator.getDataSource() instanceof DatasetDataSource dataSource)) { + return; + } + DatasetFullyQualifiedName fullyQualifiedName = dataSource.getDataset().getDatasetFullyQualifiedName(); + throw new AlgebricksException(ErrorCode.INDEX_ADVISOR_SAMPLE_NOT_FOUND, createSampleStatement(fullyQualifiedName)); + } + + private static String createSampleStatement(DatasetFullyQualifiedName dqn) { + return "ANALYZE COLLECTION `" + dqn.getDatabaseName() + "`.`" + dqn.getDataverseName() + "`.`" + + dqn.getDatasetName() + "`;"; + } + } diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdviseIndexRule.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdviseIndexRule.java index 9a7fbdd..7e34d25 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdviseIndexRule.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdviseIndexRule.java @@ -121,7 +121,16 @@ DataverseName dataverse = jobGenParams.getDataverseName(); String datasetName = jobGenParams.getDatasetName(); + if(fakeIndexProvider == null) { + // Case when CBO can't parse the plan correctly and the fake index provider is not set. + return; + } Index fakeIndex = fakeIndexProvider.getIndex(databaseName, dataverse, datasetName, indexName); + if (fakeIndex == null || !(fakeIndex.getIndexDetails() instanceof Index.ValueIndexDetails)) { + // skips secondary primary index like + // create primary index sec_primary_idx on A; + return; + } Index actualIndex = lookupIndex(databaseName, dataverse, datasetName, ((Index.ValueIndexDetails) fakeIndex.getIndexDetails()).getKeyFieldNames(), actualIndexProvider); diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdvisorConditionParser.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdvisorConditionParser.java index a86b1b5..05039aa 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdvisorConditionParser.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/AdvisorConditionParser.java @@ -47,11 +47,27 @@ public class AdvisorConditionParser { + private static class ExprRef { + private final ILogicalExpression expr; + private final ILogicalOperator op; + + public ExprRef(Mutable<ILogicalExpression> expr, ILogicalOperator op) { + this.expr = expr.getValue().cloneExpression(); + this.op = op; + } + + public ILogicalExpression getExpr() { + return expr; + } + + public ILogicalOperator getOp() { + return op; + } + } + public static ScanFilter parseScanNode(ILogicalOperator op, IOptimizationContext context) throws AlgebricksException { - - List<Mutable<ILogicalExpression>> filterExprs = new ArrayList<>(); - IVariableTypeEnvironment typeEnv = PushdownUtil.getTypeEnv(op, context); + List<ExprRef> filterExprRefs = new ArrayList<>(); ILogicalOperator tempOp = op; do { if (tempOp.getOperatorTag() == LogicalOperatorTag.SELECT) { @@ -59,35 +75,33 @@ ILogicalExpression condition = selectOp.getCondition().getValue(); List<Mutable<ILogicalExpression>> conjs = new ArrayList<>(); if (condition.splitIntoConjuncts(conjs)) { - filterExprs.addAll(conjs); + filterExprRefs.addAll(conjs.stream().map(expr -> new ExprRef(expr, selectOp)).toList()); } else { - filterExprs.add(selectOp.getCondition()); + filterExprRefs.add(new ExprRef(selectOp.getCondition(), selectOp)); } } tempOp = tempOp.getInputs().getFirst().getValue(); } while (tempOp.hasInputs()); - filterExprs = OperatorManipulationUtil.cloneExpressions(filterExprs); - - filterExprs.removeIf(expr -> !(expr.getValue() instanceof AbstractFunctionCallExpression)); + filterExprRefs.removeIf(exprRef -> !(exprRef.getExpr() instanceof AbstractFunctionCallExpression)); tempOp = op; do { if (tempOp.getOperatorTag() == LogicalOperatorTag.ASSIGN) { - replaceExprsWithAssign((AssignOperator) tempOp, filterExprs); + replaceExprsWithAssign((AssignOperator) tempOp, filterExprRefs); } tempOp = tempOp.getInputs().getFirst().getValue(); } while (tempOp.hasInputs()); List<ScanFilterCondition> filterConditions = new ArrayList<>(); - for (Mutable<ILogicalExpression> filterExpr : filterExprs) { - ScanFilterCondition filterCondition = parseCondition(filterExpr.getValue(), typeEnv); + for (ExprRef exprRef : filterExprRefs) { + IVariableTypeEnvironment typeEnv = PushdownUtil.getTypeEnv(exprRef.getOp(), context); + ScanFilterCondition filterCondition = parseCondition(exprRef.getExpr(), typeEnv); if (filterCondition != null) { filterConditions.add(filterCondition); } } - return new ScanFilter(filterConditions); } @@ -96,9 +110,7 @@ if (!(logicalExpression instanceof AbstractFunctionCallExpression expr)) { return null; } - FunctionIdentifier fi = expr.getFunctionIdentifier(); - if (!BTreeAccessMethod.INSTANCE.getOptimizableFunctions().contains(new Pair<>(fi, false))) { return null; } @@ -137,6 +149,9 @@ expr = functionCallExpr.getArguments().getFirst().getValue(); } + if (fieldNames.isEmpty()) { + return null; + } VariableReferenceExpression varRef = (VariableReferenceExpression) expr; LogicalVariable var = varRef.getVariableReference(); @@ -145,23 +160,21 @@ public static JoinFilter parseJoinNode(AbstractBinaryJoinOperator joinOp, IOptimizationContext context) throws AlgebricksException { - List<Mutable<ILogicalExpression>> joinExprs = new ArrayList<>(); - IVariableTypeEnvironment typeEnv = PushdownUtil.getTypeEnv(joinOp, context); - + List<ExprRef> joinExprs = new ArrayList<>(); ILogicalExpression joinExpression = joinOp.getCondition().getValue(); List<Mutable<ILogicalExpression>> conjs = new ArrayList<>(); if (joinExpression.splitIntoConjuncts(conjs)) { - joinExprs.addAll(conjs); + joinExprs.addAll(conjs.stream().map(expr -> new ExprRef(expr, joinOp)).toList()); } else { - joinExprs.add(joinOp.getCondition()); + joinExprs.add(new ExprRef(joinOp.getCondition(), joinOp)); } - joinExprs = OperatorManipulationUtil.cloneExpressions(joinExprs); - traverseAndReplace(joinOp, joinExprs); + traverseAndReplace(joinOp, joinExprs); List<JoinFilterCondition> joinConditions = new ArrayList<>(); - for (Mutable<ILogicalExpression> joinExpr : joinExprs) { - JoinFilterCondition joinCondition = parseJoinCondition(joinExpr.getValue(), typeEnv); + for (ExprRef joinExprRef : joinExprs) { + IVariableTypeEnvironment typeEnv = PushdownUtil.getTypeEnv(joinExprRef.getOp(), context); + JoinFilterCondition joinCondition = parseJoinCondition(joinExprRef.getExpr(), typeEnv); if (joinCondition != null) { joinConditions.add(joinCondition); } @@ -170,25 +183,22 @@ return new JoinFilter(joinConditions); } - private static void traverseAndReplace(AbstractLogicalOperator op, List<Mutable<ILogicalExpression>> exprs) { - + private static void traverseAndReplace(AbstractLogicalOperator op, List<ExprRef> exprs) { if (op.getOperatorTag() == LogicalOperatorTag.ASSIGN) { replaceExprsWithAssign((AssignOperator) op, exprs); } - for (Mutable<ILogicalOperator> input : op.getInputs()) { traverseAndReplace((AbstractLogicalOperator) input.getValue(), exprs); } - } - public static void replaceExprsWithAssign(AssignOperator assignOp, List<Mutable<ILogicalExpression>> exprs) { - for (Mutable<ILogicalExpression> filterExpr : exprs) { - if (filterExpr.getValue().getExpressionTag() == LogicalExpressionTag.CONSTANT) { + private static void replaceExprsWithAssign(AssignOperator assignOp, List<ExprRef> exprRefs) { + for (ExprRef exprRef : exprRefs) { + if (exprRef.getExpr().getExpressionTag() == LogicalExpressionTag.CONSTANT) { continue; } for (int i = 0; i < assignOp.getVariables().size(); i++) { - OperatorManipulationUtil.replaceVarWithExpr((AbstractFunctionCallExpression) filterExpr.getValue(), + OperatorManipulationUtil.replaceVarWithExpr((AbstractFunctionCallExpression) exprRef.getExpr(), assignOp.getVariables().get(i), assignOp.getExpressions().get(i).getValue()); } } diff --git a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/FakeIndexProvider.java b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/FakeIndexProvider.java index ee487dd..1b33096 100644 --- a/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/FakeIndexProvider.java +++ b/asterixdb/asterix-algebra/src/main/java/org/apache/asterix/optimizer/rules/cbo/indexadvisor/FakeIndexProvider.java @@ -20,6 +20,7 @@ import static java.util.UUID.randomUUID; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -131,6 +132,9 @@ for (Map.Entry<DatasetFullyQualifiedName, Set<List<String>>> entry : joinDataSourceFieldNamesMap.entrySet()) { DatasetFullyQualifiedName qualifiedName = entry.getKey(); Set<List<String>> fieldNames = entry.getValue(); + if (fieldNames.isEmpty()) { + continue; + } joinIndexEnumerator.init(fieldNames); Iterator<List<List<String>>> itr = joinIndexEnumerator.getIterator(); @@ -169,8 +173,8 @@ @Override public List<Index> getDatasetIndexes(String database, DataverseName dataverseName, String datasetName) throws AlgebricksException { - return filterIndexesMap.get(new DatasetFullyQualifiedName(database, dataverseName, datasetName)).values() - .stream().toList(); + return filterIndexesMap.getOrDefault(new DatasetFullyQualifiedName(database, dataverseName, datasetName), + Collections.emptyMap()).values().stream().toList(); } } diff --git a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java index 59e6dc2..aff7850 100644 --- a/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java +++ b/asterixdb/asterix-app/src/main/java/org/apache/asterix/api/common/APIFramework.java @@ -312,6 +312,14 @@ builder.setNormalizedKeyComputerFactoryProvider(format.getNormalizedKeyComputerFactoryProvider()); IndexAdvisor indexAdvisor = new IndexAdvisor(isAdviceOnly); + + if (isAdviceOnly) { + if (!physOptConf.getCBOMode() && !physOptConf.getCBOTestMode()) { + throw new CompilationException(ErrorCode.COMPILATION_ERROR, + "Index advise cannot be used without CBO mode."); + } + } + IRuleSetKind ruleSetKind = isAdviceOnly ? LOGICAL_ADVISOR : QUERY; ICompiler compiler = compilerFactory.createCompiler(plan, metadataProvider, t.getVarCounter(), ruleSetKind, indexAdvisor); diff --git a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/base/IndexAdvisor.java b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/base/IndexAdvisor.java index d381afa..1ba74ca 100644 --- a/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/base/IndexAdvisor.java +++ b/hyracks-fullstack/algebricks/algebricks-core/src/main/java/org/apache/hyracks/algebricks/core/algebra/base/IndexAdvisor.java @@ -130,7 +130,6 @@ } public String getCreateIndexClause() { - return "CREATE INDEX " + indexName + " ON `" + databaseName + "`.`" + dataverseName + "`.`" + datasetName + "`" + getKeyFieldNamesClause() + ";"; } diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/exceptions/ErrorCode.java b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/exceptions/ErrorCode.java index d7abf4c..d83270f 100644 --- a/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/exceptions/ErrorCode.java +++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/java/org/apache/hyracks/api/exceptions/ErrorCode.java @@ -174,7 +174,8 @@ INAPPLICABLE_HINT(10006), CROSS_PRODUCT_JOIN(10007), GROUP_ALL_DECOR(10008), - EXPRESSION_CANNOT_BE_CONSTANT(10009); + EXPRESSION_CANNOT_BE_CONSTANT(10009), + INDEX_ADVISOR_SAMPLE_NOT_FOUND(10010); private static final String RESOURCE_PATH = "errormsg/en.properties"; public static final String HYRACKS = "HYR"; diff --git a/hyracks-fullstack/hyracks/hyracks-api/src/main/resources/errormsg/en.properties b/hyracks-fullstack/hyracks/hyracks-api/src/main/resources/errormsg/en.properties index e9569be..8e16262 100644 --- a/hyracks-fullstack/hyracks/hyracks-api/src/main/resources/errormsg/en.properties +++ b/hyracks-fullstack/hyracks/hyracks-api/src/main/resources/errormsg/en.properties @@ -163,4 +163,5 @@ 10006 = Could not apply %1$s hint: %2$s 10007 = Encountered a cross product join 10008 = Inappropriate use of group by all with decor variables -10009 = '%1$s' expression cannot be a constant \ No newline at end of file +10009 = '%1$s' expression cannot be a constant +10010 = No samples found for the collections in this query. Create samples with: '%1$s' \ No newline at end of file -- To view, visit https://asterix-gerrit.ics.uci.edu/c/asterixdb/+/20282?usp=email To unsubscribe, or for help writing mail filters, visit https://asterix-gerrit.ics.uci.edu/settings?usp=email Gerrit-MessageType: merged Gerrit-Project: asterixdb Gerrit-Branch: master Gerrit-Change-Id: I346728dd634934416d7115a06ab15572b5a98854 Gerrit-Change-Number: 20282 Gerrit-PatchSet: 6 Gerrit-Owner: Preetham Poluparthi <[email protected]> Gerrit-Reviewer: Ali Alsuliman <[email protected]> Gerrit-Reviewer: Anon. E. Moose #1000171 Gerrit-Reviewer: Jenkins <[email protected]> Gerrit-Reviewer: Preetham Poluparthi <[email protected]>
