Parity API (#280)
This commit is contained in:
219
pkg/utils/parity.go
Normal file
219
pkg/utils/parity.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/minio/pkg/ellipses"
|
||||
)
|
||||
|
||||
// This file implements and supports ellipses pattern for
|
||||
// `minio server` command line arguments.
|
||||
|
||||
// Supported set sizes this is used to find the optimal
|
||||
// single set size.
|
||||
var setSizes = []uint64{4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
|
||||
|
||||
// getDivisibleSize - returns a greatest common divisor of
|
||||
// all the ellipses sizes.
|
||||
func getDivisibleSize(totalSizes []uint64) (result uint64) {
|
||||
gcd := func(x, y uint64) uint64 {
|
||||
for y != 0 {
|
||||
x, y = y, x%y
|
||||
}
|
||||
return x
|
||||
}
|
||||
result = totalSizes[0]
|
||||
for i := 1; i < len(totalSizes); i++ {
|
||||
result = gcd(result, totalSizes[i])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
|
||||
var isValidSetSize = func(count uint64) bool {
|
||||
return (count >= setSizes[0] && count <= setSizes[len(setSizes)-1])
|
||||
}
|
||||
|
||||
// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
|
||||
// input argument patterns, the symmetry calculation is to ensure that
|
||||
// we also use uniform number of drives common across all ellipses patterns.
|
||||
func possibleSetCountsWithSymmetry(setCounts []uint64, argPatterns []ellipses.ArgPattern) []uint64 {
|
||||
var newSetCounts = make(map[uint64]struct{})
|
||||
for _, ss := range setCounts {
|
||||
var symmetry bool
|
||||
for _, argPattern := range argPatterns {
|
||||
for _, p := range argPattern {
|
||||
if uint64(len(p.Seq)) > ss {
|
||||
symmetry = uint64(len(p.Seq))%ss == 0
|
||||
} else {
|
||||
symmetry = ss%uint64(len(p.Seq)) == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
// With no arg patterns, it is expected that user knows
|
||||
// the right symmetry, so either ellipses patterns are
|
||||
// provided (recommended) or no ellipses patterns.
|
||||
if _, ok := newSetCounts[ss]; !ok && (symmetry || argPatterns == nil) {
|
||||
newSetCounts[ss] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
setCounts = []uint64{}
|
||||
for setCount := range newSetCounts {
|
||||
setCounts = append(setCounts, setCount)
|
||||
}
|
||||
|
||||
// Not necessarily needed but it ensures to the readers
|
||||
// eyes that we prefer a sorted setCount slice for the
|
||||
// subsequent function to figure out the right common
|
||||
// divisor, it avoids loops.
|
||||
sort.Slice(setCounts, func(i, j int) bool {
|
||||
return setCounts[i] < setCounts[j]
|
||||
})
|
||||
|
||||
return setCounts
|
||||
}
|
||||
|
||||
func commonSetDriveCount(divisibleSize uint64, setCounts []uint64) (setSize uint64) {
|
||||
// prefers setCounts to be sorted for optimal behavior.
|
||||
if divisibleSize < setCounts[len(setCounts)-1] {
|
||||
return divisibleSize
|
||||
}
|
||||
|
||||
// Figure out largest value of total_drives_in_erasure_set which results
|
||||
// in least number of total_drives/total_drives_erasure_set ratio.
|
||||
prevD := divisibleSize / setCounts[0]
|
||||
for _, cnt := range setCounts {
|
||||
if divisibleSize%cnt == 0 {
|
||||
d := divisibleSize / cnt
|
||||
if d <= prevD {
|
||||
prevD = d
|
||||
setSize = cnt
|
||||
}
|
||||
}
|
||||
}
|
||||
return setSize
|
||||
}
|
||||
|
||||
// getSetIndexes returns list of indexes which provides the set size
|
||||
// on each index, this function also determines the final set size
|
||||
// The final set size has the affinity towards choosing smaller
|
||||
// indexes (total sets)
|
||||
func getSetIndexes(args []string, totalSizes []uint64, argPatterns []ellipses.ArgPattern) (setIndexes [][]uint64, err error) {
|
||||
if len(totalSizes) == 0 || len(args) == 0 {
|
||||
return nil, errors.New("invalid argument")
|
||||
}
|
||||
|
||||
setIndexes = make([][]uint64, len(totalSizes))
|
||||
for _, totalSize := range totalSizes {
|
||||
// Check if totalSize has minimum range upto setSize
|
||||
if totalSize < setSizes[0] {
|
||||
return nil, fmt.Errorf("incorrect number of endpoints provided %s", args)
|
||||
}
|
||||
}
|
||||
|
||||
commonSize := getDivisibleSize(totalSizes)
|
||||
possibleSetCounts := func(setSize uint64) (ss []uint64) {
|
||||
for _, s := range setSizes {
|
||||
if setSize%s == 0 {
|
||||
ss = append(ss, s)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
setCounts := possibleSetCounts(commonSize)
|
||||
if len(setCounts) == 0 {
|
||||
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Returns possible set counts with symmetry.
|
||||
setCounts = possibleSetCountsWithSymmetry(setCounts, argPatterns)
|
||||
if len(setCounts) == 0 {
|
||||
err = fmt.Errorf("no symmetric distribution detected with input endpoints provided %s, disks %d cannot be spread symmetrically by any supported erasure set sizes %d", args, commonSize, setSizes)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Final set size with all the symmetry accounted for.
|
||||
setSize := commonSetDriveCount(commonSize, setCounts)
|
||||
|
||||
// Check whether setSize is with the supported range.
|
||||
if !isValidSetSize(setSize) {
|
||||
err = fmt.Errorf("incorrect number of endpoints provided %s, number of disks %d is not divisible by any supported erasure set sizes %d", args, commonSize, setSizes)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range totalSizes {
|
||||
for j := uint64(0); j < totalSizes[i]/setSize; j++ {
|
||||
setIndexes[i] = append(setIndexes[i], setSize)
|
||||
}
|
||||
}
|
||||
|
||||
return setIndexes, nil
|
||||
}
|
||||
|
||||
// Return the total size for each argument patterns.
|
||||
func getTotalSizes(argPatterns []ellipses.ArgPattern) []uint64 {
|
||||
var totalSizes []uint64
|
||||
for _, argPattern := range argPatterns {
|
||||
var totalSize uint64 = 1
|
||||
for _, p := range argPattern {
|
||||
totalSize = totalSize * uint64(len(p.Seq))
|
||||
}
|
||||
totalSizes = append(totalSizes, totalSize)
|
||||
}
|
||||
return totalSizes
|
||||
}
|
||||
|
||||
// PossibleParityValues returns possible parities for input args,
|
||||
// parties are calculated in uniform manner for one zone or
|
||||
// multiple zones, ensuring that parities returned are common
|
||||
// and applicable across all zones.
|
||||
func PossibleParityValues(args ...string) ([]string, error) {
|
||||
setIndexes, err := parseEndpointSet(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maximumParity := setIndexes[0][0] / 2
|
||||
var parities []string
|
||||
for maximumParity >= 2 {
|
||||
parities = append(parities, fmt.Sprintf("EC:%d", maximumParity))
|
||||
maximumParity--
|
||||
}
|
||||
return parities, nil
|
||||
}
|
||||
|
||||
// Parses all arguments and returns an endpointSet which is a collection
|
||||
// of endpoints following the ellipses pattern, this is what is used
|
||||
// by the object layer for initializing itself.
|
||||
func parseEndpointSet(args ...string) (setIndexes [][]uint64, err error) {
|
||||
var argPatterns = make([]ellipses.ArgPattern, len(args))
|
||||
for i, arg := range args {
|
||||
patterns, err := ellipses.FindEllipsesPatterns(arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argPatterns[i] = patterns
|
||||
}
|
||||
|
||||
return getSetIndexes(args, getTotalSizes(argPatterns), argPatterns)
|
||||
}
|
||||
281
pkg/utils/parity_test.go
Normal file
281
pkg/utils/parity_test.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// Copyright (c) 2020 MinIO, Inc.
|
||||
//
|
||||
// All rights reserved
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio/pkg/ellipses"
|
||||
)
|
||||
|
||||
func TestGetDivisibleSize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
totalSizes []uint64
|
||||
result uint64
|
||||
}{{[]uint64{24, 32, 16}, 8},
|
||||
{[]uint64{32, 8, 4}, 4},
|
||||
{[]uint64{8, 8, 8}, 8},
|
||||
{[]uint64{24}, 24},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
gotGCD := getDivisibleSize(testCase.totalSizes)
|
||||
if testCase.result != gotGCD {
|
||||
t.Errorf("Expected %v, got %v", testCase.result, gotGCD)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test tests calculating set indexes.
|
||||
func TestGetSetIndexes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
args []string
|
||||
totalSizes []uint64
|
||||
indexes [][]uint64
|
||||
success bool
|
||||
}{
|
||||
// Invalid inputs.
|
||||
{
|
||||
[]string{"data{1...3}"},
|
||||
[]uint64{3},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...2}, data/controller2/export{1...4}, data/controller3/export{1...8}"},
|
||||
[]uint64{2, 4, 8},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...17}/export{1...52}"},
|
||||
[]uint64{14144},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// Valid inputs.
|
||||
{
|
||||
[]string{"data{1...27}"},
|
||||
[]uint64{27},
|
||||
[][]uint64{{9, 9, 9}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...3}/data{1...180}"},
|
||||
[]uint64{540},
|
||||
[][]uint64{{15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...2}.rack{1...4}/data{1...180}"},
|
||||
[]uint64{1440},
|
||||
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"http://host{1...2}/data{1...180}"},
|
||||
[]uint64{360},
|
||||
[][]uint64{{12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...4}, data/controller2/export{1...8}, data/controller3/export{1...12}"},
|
||||
[]uint64{4, 8, 12},
|
||||
[][]uint64{{4}, {4, 4}, {4, 4, 4}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...64}"},
|
||||
[]uint64{64},
|
||||
[][]uint64{{16, 16, 16, 16}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...24}"},
|
||||
[]uint64{24},
|
||||
[][]uint64{{12, 12}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller{1...11}/export{1...8}"},
|
||||
[]uint64{88},
|
||||
[][]uint64{{11, 11, 11, 11, 11, 11, 11, 11}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...4}"},
|
||||
[]uint64{4},
|
||||
[][]uint64{{4}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data/controller1/export{1...10}, data/controller2/export{1...10}, data/controller3/export{1...10}"},
|
||||
[]uint64{10, 10, 10},
|
||||
[][]uint64{{10}, {10}, {10}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]string{"data{1...16}/export{1...52}"},
|
||||
[]uint64{832},
|
||||
[][]uint64{{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
var argPatterns = make([]ellipses.ArgPattern, len(testCase.args))
|
||||
for i, arg := range testCase.args {
|
||||
patterns, err := ellipses.FindEllipsesPatterns(arg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected failure %s", err)
|
||||
}
|
||||
argPatterns[i] = patterns
|
||||
}
|
||||
gotIndexes, err := getSetIndexes(testCase.args, testCase.totalSizes, argPatterns)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Expected success but failed instead %s", err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Expected failure but passed instead")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.indexes, gotIndexes) {
|
||||
t.Errorf("Expected %v, got %v", testCase.indexes, gotIndexes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test tests possible parities returned for any input args
|
||||
func TestPossibleParities(t *testing.T) {
|
||||
testCases := []struct {
|
||||
arg string
|
||||
parities []string
|
||||
success bool
|
||||
}{
|
||||
// Tests invalid inputs.
|
||||
{
|
||||
"...",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// No range specified.
|
||||
{
|
||||
"{...}",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// Invalid range.
|
||||
{
|
||||
"http://minio{2...3}/export/set{1...0}",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// Range cannot be smaller than 4 minimum.
|
||||
{
|
||||
"/export{1..2}",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// Unsupported characters.
|
||||
{
|
||||
"/export/test{1...2O}",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
// Tests valid inputs.
|
||||
{
|
||||
"{1...27}",
|
||||
[]string{"EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"/export/set{1...64}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// Valid input for distributed setup.
|
||||
{
|
||||
"http://minio{2...3}/export/set{1...64}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// Supporting some advanced cases.
|
||||
{
|
||||
"http://minio{1...64}.mydomain.net/data",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"http://rack{1...4}.mydomain.minio{1...16}/data",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// Supporting kubernetes cases.
|
||||
{
|
||||
"http://minio{0...15}.mydomain.net/data{0...1}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// No host regex, just disks.
|
||||
{
|
||||
"http://server1/data{1...32}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// No host regex, just disks with two position numerics.
|
||||
{
|
||||
"http://server1/data{01...32}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// More than 2 ellipses are supported as well.
|
||||
{
|
||||
"http://minio{2...3}/export/set{1...64}/test{1...2}",
|
||||
[]string{"EC:8", "EC:7", "EC:6", "EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// More than 1 ellipses per argument for standalone setup.
|
||||
{
|
||||
"/export{1...10}/disk{1...10}",
|
||||
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// IPv6 ellipses with hexadecimal expansion
|
||||
{
|
||||
"http://[2001:3984:3989::{1...a}]/disk{1...10}",
|
||||
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
// IPv6 ellipses with hexadecimal expansion with 3 position numerics.
|
||||
{
|
||||
"http://[2001:3984:3989::{001...00a}]/disk{1...10}",
|
||||
[]string{"EC:5", "EC:4", "EC:3", "EC:2"},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run("", func(t *testing.T) {
|
||||
gotPs, err := PossibleParityValues(testCase.arg)
|
||||
if err != nil && testCase.success {
|
||||
t.Errorf("Expected success but failed instead %s", err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Errorf("Expected failure but passed instead")
|
||||
}
|
||||
if !reflect.DeepEqual(testCase.parities, gotPs) {
|
||||
t.Errorf("Expected %v, got %v", testCase.parities, gotPs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user