Skip to content

Commit

Permalink
server/placement: add placement constraint config parser (#1256)
Browse files Browse the repository at this point in the history
  • Loading branch information
disksing authored Sep 28, 2018
1 parent 4bd89ab commit 25d9967
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 0 deletions.
38 changes: 38 additions & 0 deletions server/placement/constraint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2018 PingCAP, Inc.
//
// Licensed 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package placement

// Config is consist of a list of constraints.
type Config struct {
Constraints []*Constraint
}

// Constraint represents a user-defined region placement rule. Each constraint
// is configured as an expression, for example 'count(zone:z1,rack:r1,host)>=3'.
type Constraint struct {
Function string // One of "count", "label_values", "count_leader", "isolation_level".
Filters []Filter // "key:value" formed parameters.
Labels []string // "key" formed parameters.
Op string // One of "<", "<=", "=", ">=", ">".
Value int // Expected expression evaluate value.
}

// Filter is used for filtering replicas of a region. The form in the
// configuration is "key:value", which appears in the function argument of the
// expression.
type Filter struct {
Key, Value string
}

var functionList = []string{"count", "label_values", "count_leader", "isolation_level"}
130 changes: 130 additions & 0 deletions server/placement/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2018 PingCAP, Inc.
//
// Licensed 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package placement

import (
"regexp"
"strconv"
"strings"

"github.com/pkg/errors"
)

// ParseConfig parses a user configuration string.
func ParseConfig(str string) (*Config, error) {
var config Config
for _, sub := range strings.Split(str, ";") {
if len(sub) > 0 {
c, err := parseConstraint(sub)
if err != nil {
return nil, err
}
config.Constraints = append(config.Constraints, c)
}
}
return &config, nil
}

func parseConstraint(str string) (*Constraint, error) {
re := regexp.MustCompile(`^(.+)\((.*)\)\s*([<>=]+)\s*([\+\-0-9]+)\s*$`)
matches := re.FindStringSubmatch(str)
if len(matches) != 5 {
return nil, errors.Errorf("bad format constraint '%s'", str)
}
function, err := parserFunctionName(matches[1])
if err != nil {
return nil, err
}
filters, labels, err := parseArguments(matches[2])
if err != nil {
return nil, err
}
op, err := parseOp(matches[3])
if err != nil {
return nil, err
}
value, err := parseValue(matches[4])
if err != nil {
return nil, err
}
return &Constraint{
Function: function,
Filters: filters,
Labels: labels,
Op: op,
Value: value,
}, nil
}

func parserFunctionName(str string) (string, error) {
str = strings.TrimSpace(str)
for _, f := range functionList {
if str == f {
return str, nil
}
}
return "", errors.Errorf("unexpected function name '%s'", str)
}

func parseArguments(str string) (filters []Filter, labels []string, err error) {
str = strings.TrimSpace(str)
if len(str) == 0 {
return nil, nil, nil
}
for _, sub := range strings.Split(str, ",") {
if idx := strings.Index(sub, ":"); idx != -1 {
key, err := parseArgument(sub[:idx])
if err != nil {
return nil, nil, err
}
value, err := parseArgument(sub[idx+1:])
if err != nil {
return nil, nil, err
}
filters = append(filters, Filter{Key: key, Value: value})
} else {
label, err := parseArgument(sub)
if err != nil {
return nil, nil, err
}
labels = append(labels, label)
}
}
return
}

func parseArgument(str string) (string, error) {
str = strings.TrimSpace(str)
if ok, _ := regexp.MatchString(`^[a-zA-Z0-9_\-.]+$`, str); !ok {
return "", errors.Errorf("invalid argument '%s'", str)
}
return str, nil
}

func parseOp(str string) (string, error) {
str = strings.TrimSpace(str)
switch str {
case "<", "<=", "=", ">=", ">":
return str, nil
}
return "", errors.Errorf(`expect one of "<", "<=", "=", ">=", ">", but got '%s'`, str)
}

func parseValue(str string) (int, error) {
n, err := strconv.Atoi(strings.TrimSpace(str))
if err != nil {
return 0, errors.Errorf("expected integer, but got '%s'", str)
}
return n, nil
}
95 changes: 95 additions & 0 deletions server/placement/placement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright 2018 PingCAP, Inc.
//
// Licensed 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package placement

import (
"testing"

. "github.com/pingcap/check"
)

func TestPlacement(t *testing.T) {
TestingT(t)
}

var _ = Suite(&testPlacementSuite{})

type testPlacementSuite struct{}

func (s *testPlacementSuite) TestConfigParse(c *C) {
cases := []struct {
source string
config *Config
}{
{source: ``, config: &Config{}},
{source: `;`, config: &Config{}},
{
source: `count()=3`,
config: s.config(s.constraint("count", "=", 3)),
},
{
source: `count(zone:z1,region:r3)>=5`,
config: s.config(s.constraint("count", ">=", 5, "zone", "z1", "region", "r3")),
},
{
source: `count()>5;label_values(zone:z1,host,ssd)>-1`,
config: s.config(s.constraint("count", ">", 5), s.constraint("label_values", ">", -1, "zone", "z1", "host", "", "ssd", "")),
},
{
source: " count ( ) <5 ;; \tlabel_values\t ( zone : z1 , \thost , ssd ) > -1 ;;;",
config: s.config(s.constraint("count", "<", 5), s.constraint("label_values", ">", -1, "zone", "z1", "host", "", "ssd", "")),
},
// Wrong format configs.
{source: "count=3"},
{source: "count()=abc"},
{source: "count()=<3"},
{source: "count()"},
{source: "+count()"},
{source: "count(a=b)=1"},
{source: "count(a;b;c)=1"},
}

for _, t := range cases {
config, err := ParseConfig(t.source)
if t.config != nil {
c.Assert(err, IsNil)
c.Assert(config, DeepEquals, t.config)
} else {
c.Assert(err, NotNil)
}
}
}

func (s *testPlacementSuite) constraint(function string, op string, value int, argPairs ...string) *Constraint {
var filters []Filter
var labels []string
for i := 0; i < len(argPairs); i += 2 {
if argPairs[i+1] == "" {
labels = append(labels, argPairs[i])
} else {
filters = append(filters, Filter{Key: argPairs[i], Value: argPairs[i+1]})
}
}
return &Constraint{
Function: function,
Filters: filters,
Labels: labels,
Op: op,
Value: value,
}
}

func (s *testPlacementSuite) config(constraints ...*Constraint) *Config {
return &Config{Constraints: constraints}
}

0 comments on commit 25d9967

Please sign in to comment.