org.apache.hadoop.yarn.util.constraint.PlacementConstraintParser Maven / Gradle / Ivy
/**
* 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.hadoop.yarn.util.constraint;
import org.apache.hadoop.thirdparty.com.google.common.base.Strings;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.yarn.api.records.AllocationTagNamespaceType;
import org.apache.hadoop.yarn.api.records.NodeAttributeOpCode;
import org.apache.hadoop.yarn.api.resource.PlacementConstraint;
import org.apache.hadoop.yarn.api.resource.PlacementConstraint.AbstractConstraint;
import org.apache.hadoop.yarn.api.resource.PlacementConstraint.TargetExpression;
import org.apache.hadoop.yarn.api.resource.PlacementConstraints;
import org.apache.hadoop.yarn.api.resource.PlacementConstraints.PlacementTargets;
import java.util.ArrayList;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.StringTokenizer;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.Set;
import java.util.TreeSet;
import java.util.HashSet;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Placement constraint expression parser.
*/
@InterfaceAudience.Public
@InterfaceStability.Unstable
public final class PlacementConstraintParser {
public static final char EXPRESSION_VAL_DELIM = ',';
private static final char EXPRESSION_DELIM = ':';
private static final char KV_SPLIT_DELIM = '=';
private static final char BRACKET_START = '(';
private static final char BRACKET_END = ')';
private static final char NAMESPACE_DELIM = '/';
private static final String KV_NE_DELIM = "!=";
private static final String IN = "in";
private static final String NOT_IN = "notin";
private static final String AND = "and";
private static final String OR = "or";
private static final String CARDINALITY = "cardinality";
private static final String SCOPE_NODE = PlacementConstraints.NODE;
private static final String SCOPE_RACK = PlacementConstraints.RACK;
private PlacementConstraintParser() {
// Private constructor for this utility class.
}
/**
* Constraint Parser used to parse placement constraints from a
* given expression.
*/
public static abstract class ConstraintParser {
private final ConstraintTokenizer tokenizer;
public ConstraintParser(ConstraintTokenizer tk){
this.tokenizer = tk;
}
void validate() throws PlacementConstraintParseException {
tokenizer.validate();
}
void shouldHaveNext()
throws PlacementConstraintParseException {
if (!tokenizer.hasMoreElements()) {
throw new PlacementConstraintParseException("Expecting more tokens");
}
}
String nextToken() {
return this.tokenizer.nextElement().trim();
}
boolean hasMoreTokens() {
return this.tokenizer.hasMoreElements();
}
int toInt(String name) throws PlacementConstraintParseException {
try {
return Integer.parseInt(name);
} catch (NumberFormatException e) {
throw new PlacementConstraintParseException(
"Expecting an Integer, but get " + name);
}
}
TargetExpression parseNameSpace(String targetTag)
throws PlacementConstraintParseException {
int i = targetTag.lastIndexOf(NAMESPACE_DELIM);
if (i != -1) {
String namespace = targetTag.substring(0, i);
for (AllocationTagNamespaceType type :
AllocationTagNamespaceType.values()) {
if (type.getTypeKeyword().equals(namespace)) {
return PlacementTargets.allocationTagWithNamespace(
namespace, targetTag.substring(i+1));
}
}
throw new PlacementConstraintParseException(
"Invalid namespace prefix: " + namespace);
} else {
return PlacementTargets.allocationTag(targetTag);
}
}
String parseScope(String scopeString)
throws PlacementConstraintParseException {
if (scopeString.equalsIgnoreCase(SCOPE_NODE)) {
return SCOPE_NODE;
} else if (scopeString.equalsIgnoreCase(SCOPE_RACK)) {
return SCOPE_RACK;
} else {
throw new PlacementConstraintParseException(
"expecting scope to " + SCOPE_NODE + " or " + SCOPE_RACK
+ ", but met " + scopeString);
}
}
public AbstractConstraint tryParse() {
try {
return parse();
} catch (PlacementConstraintParseException e) {
// unable to parse, simply return null
return null;
}
}
public abstract AbstractConstraint parse()
throws PlacementConstraintParseException;
}
/**
* Tokenizer interface that used to parse an expression. It first
* validates if the syntax of the given expression is valid, then traverse
* the expression and parse it to an enumeration of strings. Each parsed
* string can be further consumed by a {@link ConstraintParser} and
* transformed to a {@link AbstractConstraint}.
*/
public interface ConstraintTokenizer extends Enumeration {
/**
* Validate the schema before actual parsing the expression.
* @throws PlacementConstraintParseException
*/
default void validate() throws PlacementConstraintParseException {
// do nothing
}
}
/**
* A basic tokenizer that splits an expression by a given delimiter.
*/
public static class BaseStringTokenizer implements ConstraintTokenizer {
private final StringTokenizer tokenizer;
BaseStringTokenizer(String expr, String delimiter) {
this.tokenizer = new StringTokenizer(expr, delimiter);
}
@Override
public boolean hasMoreElements() {
return tokenizer.hasMoreTokens();
}
@Override
public String nextElement() {
return tokenizer.nextToken();
}
}
/**
* Tokenizer used to parse conjunction form of a constraint expression,
* [AND|OR](C1:C2:...:Cn). Each Cn is a constraint expression.
*/
public static final class ConjunctionTokenizer
implements ConstraintTokenizer {
private final String expression;
private Iterator iterator;
private ConjunctionTokenizer(String expr) {
this.expression = expr;
}
// Traverse the expression and try to get a list of parsed elements
// based on schema.
@Override
public void validate() throws PlacementConstraintParseException {
List parsedElements = new ArrayList<>();
// expression should start with AND or OR
String op;
if (expression.startsWith(AND) ||
expression.startsWith(AND.toUpperCase())) {
op = AND;
} else if(expression.startsWith(OR) ||
expression.startsWith(OR.toUpperCase())) {
op = OR;
} else {
throw new PlacementConstraintParseException(
"Excepting starting with \"" + AND + "\" or \"" + OR + "\","
+ " but met " + expression);
}
parsedElements.add(op);
Pattern p = Pattern.compile("\\((.*)\\)");
Matcher m = p.matcher(expression);
if (!m.find()) {
throw new PlacementConstraintParseException("Unexpected format,"
+ " expecting [AND|OR](A:B...) "
+ "but current expression is " + expression);
}
String childStrs = m.group(1);
MultipleConstraintsTokenizer ct =
new MultipleConstraintsTokenizer(childStrs);
ct.validate();
while(ct.hasMoreElements()) {
parsedElements.add(ct.nextElement());
}
this.iterator = parsedElements.iterator();
}
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public String nextElement() {
return iterator.next();
}
}
/**
* Tokenizer used to parse allocation tags expression, which should be
* in tag(numOfAllocations) syntax.
*/
public static class SourceTagsTokenizer implements ConstraintTokenizer {
private final String expression;
private StringTokenizer st;
private Iterator iterator;
public SourceTagsTokenizer(String expr) {
this.expression = expr;
st = new StringTokenizer(expr, String.valueOf(BRACKET_START));
}
@Override
public void validate() throws PlacementConstraintParseException {
ArrayList parsedValues = new ArrayList<>();
if (st.countTokens() != 2 || !this.expression.
endsWith(String.valueOf(BRACKET_END))) {
throw new PlacementConstraintParseException(
"Expecting source allocation tag to be specified"
+ " sourceTag(numOfAllocations) syntax,"
+ " but met " + expression);
}
String sourceTag = st.nextToken();
parsedValues.add(sourceTag);
String str = st.nextToken();
String num = str.substring(0, str.length() - 1);
try {
Integer.parseInt(num);
parsedValues.add(num);
} catch (NumberFormatException e) {
throw new PlacementConstraintParseException("Value of the expression"
+ " must be an integer, but met " + num);
}
iterator = parsedValues.iterator();
}
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public String nextElement() {
return iterator.next();
}
}
/**
* Tokenizer used to handle a placement spec composed by multiple
* constraint expressions. Each of them is delimited with the
* given delimiter, e.g ':'.
*/
public static class MultipleConstraintsTokenizer
implements ConstraintTokenizer {
private final String expr;
private Iterator iterator;
public MultipleConstraintsTokenizer(String expression) {
this.expr = expression;
}
@Override
public void validate() throws PlacementConstraintParseException {
ArrayList parsedElements = new ArrayList<>();
char[] arr = expr.toCharArray();
// Memorize the location of each delimiter in a stack,
// removes invalid delimiters that embraced in brackets.
Stack stack = new Stack<>();
for (int i=0; i it = stack.iterator();
int currentPos = 0;
while (it.hasNext()) {
int pos = it.next();
String sub = expr.substring(currentPos, pos);
if (sub != null && !sub.isEmpty()) {
parsedElements.add(sub);
}
currentPos = pos+1;
}
if (currentPos < expr.length()) {
parsedElements.add(expr.substring(currentPos, expr.length()));
}
}
iterator = parsedElements.iterator();
}
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public String nextElement() {
return iterator.next();
}
}
/**
* Constraint parser used to parse a given target expression.
*/
public static class NodeConstraintParser extends ConstraintParser {
public NodeConstraintParser(String expression) {
super(new BaseStringTokenizer(expression,
String.valueOf(EXPRESSION_VAL_DELIM)));
}
@Override
public AbstractConstraint parse()
throws PlacementConstraintParseException {
PlacementConstraint.AbstractConstraint placementConstraints = null;
String attributeName = "";
NodeAttributeOpCode opCode = NodeAttributeOpCode.EQ;
String scope = SCOPE_NODE;
Set constraintEntities = new TreeSet<>();
while (hasMoreTokens()) {
String currentTag = nextToken();
StringTokenizer attributeKV = getAttributeOpCodeTokenizer(currentTag);
// Usually there will be only one k=v pair. However in case when
// multiple values are present for same attribute, it will also be
// coming as next token. for example, java=1.8,1.9 or python!=2.
if (attributeKV.countTokens() > 1) {
opCode = getAttributeOpCode(currentTag);
attributeName = attributeKV.nextToken();
currentTag = attributeKV.nextToken();
}
constraintEntities.add(currentTag);
}
if(attributeName.isEmpty()) {
throw new PlacementConstraintParseException(
"expecting valid expression like k=v or k!=v, but get "
+ constraintEntities);
}
TargetExpression target = null;
if (!constraintEntities.isEmpty()) {
target = PlacementTargets.nodeAttribute(attributeName,
constraintEntities
.toArray(new String[constraintEntities.size()]));
}
placementConstraints = PlacementConstraints
.targetNodeAttribute(scope, opCode, target);
return placementConstraints;
}
private StringTokenizer getAttributeOpCodeTokenizer(String currentTag) {
StringTokenizer attributeKV = new StringTokenizer(currentTag,
KV_NE_DELIM);
// Try with '!=' delim as well.
if (attributeKV.countTokens() < 2) {
attributeKV = new StringTokenizer(currentTag,
String.valueOf(KV_SPLIT_DELIM));
}
return attributeKV;
}
/**
* Below conditions are validated.
* java=8 : OpCode = EQUALS
* java!=8 : OpCode = NEQUALS
* @param currentTag tag
* @return Attribute op code.
*/
private NodeAttributeOpCode getAttributeOpCode(String currentTag)
throws PlacementConstraintParseException {
if (currentTag.contains(KV_NE_DELIM)) {
return NodeAttributeOpCode.NE;
} else if (currentTag.contains(String.valueOf(KV_SPLIT_DELIM))) {
return NodeAttributeOpCode.EQ;
}
throw new PlacementConstraintParseException(
"expecting valid expression like k=v or k!=v, but get "
+ currentTag);
}
}
/**
* Constraint parser used to parse a given target expression, such as
* "NOTIN, NODE, foo, bar".
*/
public static class TargetConstraintParser extends ConstraintParser {
public TargetConstraintParser(String expression) {
super(new BaseStringTokenizer(expression,
String.valueOf(EXPRESSION_VAL_DELIM)));
}
@Override
public AbstractConstraint parse()
throws PlacementConstraintParseException {
PlacementConstraint.AbstractConstraint placementConstraints = null;
String op = nextToken();
if (op.equalsIgnoreCase(IN) || op.equalsIgnoreCase(NOT_IN)) {
String scope = nextToken();
scope = parseScope(scope);
Set targetExpressions = new HashSet<>();
while(hasMoreTokens()) {
String tag = nextToken();
TargetExpression t = parseNameSpace(tag);
targetExpressions.add(t);
}
TargetExpression[] targetArr = targetExpressions.toArray(
new TargetExpression[targetExpressions.size()]);
if (op.equalsIgnoreCase(IN)) {
placementConstraints = PlacementConstraints
.targetIn(scope, targetArr);
} else {
placementConstraints = PlacementConstraints
.targetNotIn(scope, targetArr);
}
} else {
throw new PlacementConstraintParseException(
"expecting " + IN + " or " + NOT_IN + ", but get " + op);
}
return placementConstraints;
}
}
/**
* Constraint parser used to parse a given target expression, such as
* "cardinality, NODE, foo, 0, 1".
*/
public static class CardinalityConstraintParser extends ConstraintParser {
public CardinalityConstraintParser(String expr) {
super(new BaseStringTokenizer(expr,
String.valueOf(EXPRESSION_VAL_DELIM)));
}
@Override
public AbstractConstraint parse()
throws PlacementConstraintParseException {
String op = nextToken();
if (!op.equalsIgnoreCase(CARDINALITY)) {
throw new PlacementConstraintParseException("expecting " + CARDINALITY
+ " , but met " + op);
}
shouldHaveNext();
String scope = nextToken();
scope = parseScope(scope);
Stack resetElements = new Stack<>();
while(hasMoreTokens()) {
resetElements.add(nextToken());
}
// At least 3 elements
if (resetElements.size() < 3) {
throw new PlacementConstraintParseException(
"Invalid syntax for a cardinality expression, expecting"
+ " \"cardinality,SCOPE,TARGET_TAG,...,TARGET_TAG,"
+ "MIN_CARDINALITY,MAX_CARDINALITY\" at least 5 elements,"
+ " but only " + (resetElements.size() + 2) + " is given.");
}
String maxCardinalityStr = resetElements.pop();
int max = toInt(maxCardinalityStr);
String minCardinalityStr = resetElements.pop();
int min = toInt(minCardinalityStr);
Set targetExpressions = new HashSet<>();
while (!resetElements.empty()) {
String tag = resetElements.pop();
TargetExpression t = parseNameSpace(tag);
targetExpressions.add(t);
}
TargetExpression[] targetArr = targetExpressions.toArray(
new TargetExpression[targetExpressions.size()]);
return PlacementConstraints.targetCardinality(scope, min, max, targetArr);
}
}
/**
* Parser used to parse conjunction form of constraints, such as
* AND(A, ..., B), OR(A, ..., B).
*/
public static class ConjunctionConstraintParser extends ConstraintParser {
public ConjunctionConstraintParser(String expr) {
super(new ConjunctionTokenizer(expr));
}
@Override
public AbstractConstraint parse() throws PlacementConstraintParseException {
// do pre-process, validate input.
validate();
String op = nextToken();
shouldHaveNext();
List constraints = new ArrayList<>();
while(hasMoreTokens()) {
// each child expression can be any valid form of
// constraint expressions.
String constraintStr = nextToken();
AbstractConstraint constraint = parseExpression(constraintStr);
constraints.add(constraint);
}
if (AND.equalsIgnoreCase(op)) {
return PlacementConstraints.and(
constraints.toArray(
new AbstractConstraint[constraints.size()]));
} else if (OR.equalsIgnoreCase(op)) {
return PlacementConstraints.or(
constraints.toArray(
new AbstractConstraint[constraints.size()]));
} else {
throw new PlacementConstraintParseException(
"Unexpected conjunction operator : " + op
+ ", expecting " + AND + " or " + OR);
}
}
}
/**
* A helper class to encapsulate source tags and allocations in the
* placement specification.
*/
public static final class SourceTags {
private String tag;
private int num;
private SourceTags(String sourceTag, int number) {
this.tag = sourceTag;
this.num = number;
}
public static SourceTags emptySourceTags() {
return new SourceTags("", 0);
}
public boolean isEmpty() {
return Strings.isNullOrEmpty(tag) && num == 0;
}
public String getTag() {
return this.tag;
}
public int getNumOfAllocations() {
return this.num;
}
/**
* Parses source tags from expression "sourceTags(numOfAllocations)".
* @param expr
* @return source tags, see {@link SourceTags}
* @throws PlacementConstraintParseException
*/
public static SourceTags parseFrom(String expr)
throws PlacementConstraintParseException {
SourceTagsTokenizer stt = new SourceTagsTokenizer(expr);
stt.validate();
// During validation we already checked the number of parsed elements.
String allocTag = stt.nextElement();
int allocNum = Integer.parseInt(stt.nextElement());
return new SourceTags(allocTag, allocNum);
}
}
/**
* Parses a given constraint expression to a {@link AbstractConstraint},
* this expression can be any valid form of constraint expressions.
*
* @param constraintStr expression string
* @return a parsed {@link AbstractConstraint}
* @throws PlacementConstraintParseException when given expression
* is malformed
*/
public static AbstractConstraint parseExpression(String constraintStr)
throws PlacementConstraintParseException {
// Try parse given expression with all allowed constraint parsers,
// fails if no one could parse it.
TargetConstraintParser tp = new TargetConstraintParser(constraintStr);
Optional constraintOptional =
Optional.ofNullable(tp.tryParse());
if (!constraintOptional.isPresent()) {
CardinalityConstraintParser cp =
new CardinalityConstraintParser(constraintStr);
constraintOptional = Optional.ofNullable(cp.tryParse());
if (!constraintOptional.isPresent()) {
ConjunctionConstraintParser jp =
new ConjunctionConstraintParser(constraintStr);
constraintOptional = Optional.ofNullable(jp.tryParse());
}
if (!constraintOptional.isPresent()) {
NodeConstraintParser np =
new NodeConstraintParser(constraintStr);
constraintOptional = Optional.ofNullable(np.tryParse());
}
if (!constraintOptional.isPresent()) {
throw new PlacementConstraintParseException(
"Invalid constraint expression " + constraintStr);
}
}
return constraintOptional.get();
}
/**
* Parses a placement constraint specification. A placement constraint spec
* is a composite expression which is composed by multiple sub constraint
* expressions delimited by ":". With following syntax:
*
* Tag1(N1),P1:Tag2(N2),P2:...:TagN(Nn),Pn
*
* where TagN(Nn) is a key value pair to determine the source
* allocation tag and the number of allocations, such as:
*
* foo(3)
*
* Optional when using NodeAttribute Constraint.
*
* and where Pn can be any form of a valid constraint expression,
* such as:
*
*
* - in,node,foo,bar
* - notin,node,foo,bar,1,2
* - and(notin,node,foo:notin,node,bar)
*
*
* and NodeAttribute Constraint such as
*
*
* - yarn.rm.io/foo=true
* - java=1.7,1.8
*
* @param expression expression string.
* @return a map of source tags to placement constraint mapping.
* @throws PlacementConstraintParseException
*/
public static Map parsePlacementSpec(
String expression) throws PlacementConstraintParseException {
// Continue handling for application tag based constraint otherwise.
// Respect insertion order.
Map result = new LinkedHashMap<>();
PlacementConstraintParser.ConstraintTokenizer tokenizer =
new PlacementConstraintParser.MultipleConstraintsTokenizer(expression);
tokenizer.validate();
while (tokenizer.hasMoreElements()) {
String specStr = tokenizer.nextElement();
// each spec starts with sourceAllocationTag(numOfContainers) and
// followed by a constraint expression.
// foo(4),Pn
final SourceTags st;
PlacementConstraint constraint;
String delimiter = new String(new char[]{'[', BRACKET_END, ']',
EXPRESSION_VAL_DELIM});
String[] splitted = specStr.split(delimiter, 2);
if (splitted.length == 2) {
st = SourceTags.parseFrom(splitted[0] + String.valueOf(BRACKET_END));
constraint = PlacementConstraintParser.parseExpression(splitted[1]).
build();
} else if (splitted.length == 1) {
// Either Node Attribute Constraint or Source Allocation Tag alone
NodeConstraintParser np = new NodeConstraintParser(specStr);
Optional constraintOptional =
Optional.ofNullable(np.tryParse());
if (constraintOptional.isPresent()) {
st = SourceTags.emptySourceTags();
constraint = constraintOptional.get().build();
} else {
st = SourceTags.parseFrom(specStr);
constraint = null;
}
} else {
throw new PlacementConstraintParseException(
"Unexpected placement constraint expression " + specStr);
}
result.put(st, constraint);
}
// Validation
Set sourceTagSet = result.keySet();
if (sourceTagSet.stream()
.filter(sourceTags -> sourceTags.isEmpty())
.findAny()
.isPresent()) {
// Source tags, e.g foo(3), is optional for a node-attribute constraint,
// but when source tags is absent, the parser only accept single
// constraint expression to avoid ambiguous semantic. This is because
// DS AM is requesting number of containers per the number specified
// in the source tags, we do overwrite when there is no source tags
// with num_containers argument from commandline. If that is partially
// missed in the constraints, we don't know if it is ought to
// overwritten or not.
if (result.size() != 1) {
throw new PlacementConstraintParseException(
"Source allocation tags is required for a multi placement"
+ " constraint expression.");
}
}
return result;
}
}