github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/base/store_spec.go (about)

     1  // Copyright 2016 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package base
    12  
    13  import (
    14  	"bytes"
    15  	"fmt"
    16  	"io/ioutil"
    17  	"net"
    18  	"os"
    19  	"path/filepath"
    20  	"regexp"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/cockroachdb/cockroach/pkg/cli/cliflags"
    26  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    27  	"github.com/cockroachdb/cockroach/pkg/util/humanizeutil"
    28  	"github.com/cockroachdb/cockroach/pkg/util/netutil"
    29  	"github.com/cockroachdb/errors"
    30  	humanize "github.com/dustin/go-humanize"
    31  	"github.com/spf13/pflag"
    32  )
    33  
    34  // This file implements method receivers for members of server.Config struct
    35  // -- 'Stores' and 'JoinList', which satisfies pflag's value interface
    36  
    37  // MinimumStoreSize is the smallest size in bytes that a store can have. This
    38  // number is based on config's defaultZoneConfig's RangeMaxBytes, which is
    39  // extremely stable. To avoid adding the dependency on config here, it is just
    40  // hard coded to 640MiB.
    41  const MinimumStoreSize = 10 * 64 << 20
    42  
    43  // GetAbsoluteStorePath takes a (possibly relative) and returns the absolute path.
    44  // Returns an error if the path begins with '~' or Abs fails.
    45  // 'fieldName' is used in error strings.
    46  func GetAbsoluteStorePath(fieldName string, p string) (string, error) {
    47  	if p[0] == '~' {
    48  		return "", fmt.Errorf("%s cannot start with '~': %s", fieldName, p)
    49  	}
    50  
    51  	ret, err := filepath.Abs(p)
    52  	if err != nil {
    53  		return "", errors.Wrapf(err, "could not find absolute path for %s %s", fieldName, p)
    54  	}
    55  	return ret, nil
    56  }
    57  
    58  // SizeSpec contains size in different kinds of formats supported by CLI(%age, bytes).
    59  type SizeSpec struct {
    60  	// InBytes is used for calculating free space and making rebalancing
    61  	// decisions. Zero indicates that there is no maximum size. This value is not
    62  	// actually used by the engine and thus not enforced.
    63  	InBytes int64
    64  	Percent float64
    65  }
    66  
    67  type intInterval struct {
    68  	min *int64
    69  	max *int64
    70  }
    71  
    72  type floatInterval struct {
    73  	min *float64
    74  	max *float64
    75  }
    76  
    77  // NewSizeSpec parses the string passed into a --size flag and returns a
    78  // SizeSpec if it is correctly parsed.
    79  func NewSizeSpec(
    80  	value string, bytesRange *intInterval, percentRange *floatInterval,
    81  ) (SizeSpec, error) {
    82  	var size SizeSpec
    83  	if fractionRegex.MatchString(value) {
    84  		percentFactor := 100.0
    85  		factorValue := value
    86  		if value[len(value)-1] == '%' {
    87  			percentFactor = 1.0
    88  			factorValue = value[:len(value)-1]
    89  		}
    90  		var err error
    91  		size.Percent, err = strconv.ParseFloat(factorValue, 64)
    92  		size.Percent *= percentFactor
    93  		if err != nil {
    94  			return SizeSpec{}, fmt.Errorf("could not parse store size (%s) %s", value, err)
    95  		}
    96  		if percentRange != nil {
    97  			if (percentRange.min != nil && size.Percent < *percentRange.min) ||
    98  				(percentRange.max != nil && size.Percent > *percentRange.max) {
    99  				return SizeSpec{}, fmt.Errorf(
   100  					"store size (%s) must be between %f%% and %f%%",
   101  					value,
   102  					*percentRange.min,
   103  					*percentRange.max,
   104  				)
   105  			}
   106  		}
   107  	} else {
   108  		var err error
   109  		size.InBytes, err = humanizeutil.ParseBytes(value)
   110  		if err != nil {
   111  			return SizeSpec{}, fmt.Errorf("could not parse store size (%s) %s", value, err)
   112  		}
   113  		if bytesRange != nil {
   114  			if bytesRange.min != nil && size.InBytes < *bytesRange.min {
   115  				return SizeSpec{}, fmt.Errorf("store size (%s) must be larger than %s", value,
   116  					humanizeutil.IBytes(*bytesRange.min))
   117  			}
   118  			if bytesRange.max != nil && size.InBytes > *bytesRange.max {
   119  				return SizeSpec{}, fmt.Errorf("store size (%s) must be smaller than %s", value,
   120  					humanizeutil.IBytes(*bytesRange.max))
   121  			}
   122  		}
   123  	}
   124  	return size, nil
   125  }
   126  
   127  // String returns a string representation of the SizeSpec. This is part
   128  // of pflag's value interface.
   129  func (ss *SizeSpec) String() string {
   130  	var buffer bytes.Buffer
   131  	if ss.InBytes != 0 {
   132  		fmt.Fprintf(&buffer, "--size=%s,", humanizeutil.IBytes(ss.InBytes))
   133  	}
   134  	if ss.Percent != 0 {
   135  		fmt.Fprintf(&buffer, "--size=%s%%,", humanize.Ftoa(ss.Percent))
   136  	}
   137  	return buffer.String()
   138  }
   139  
   140  // Type returns the underlying type in string form. This is part of pflag's
   141  // value interface.
   142  func (ss *SizeSpec) Type() string {
   143  	return "SizeSpec"
   144  }
   145  
   146  var _ pflag.Value = &SizeSpec{}
   147  
   148  // Set adds a new value to the StoreSpecValue. It is the important part of
   149  // pflag's value interface.
   150  func (ss *SizeSpec) Set(value string) error {
   151  	spec, err := NewSizeSpec(value, nil, nil)
   152  	if err != nil {
   153  		return err
   154  	}
   155  	ss.InBytes = spec.InBytes
   156  	ss.Percent = spec.Percent
   157  	return nil
   158  }
   159  
   160  // StoreSpec contains the details that can be specified in the cli pertaining
   161  // to the --store flag.
   162  type StoreSpec struct {
   163  	Path       string
   164  	Size       SizeSpec
   165  	InMemory   bool
   166  	Attributes roachpb.Attributes
   167  	// StickyInMemoryEngineID is a unique identifier associated with a given
   168  	// store which will remain in memory even after the default Engine close
   169  	// until it has been explicitly cleaned up by CleanupStickyInMemEngine[s]
   170  	// or the process has been terminated.
   171  	// This only applies to in-memory storage engine.
   172  	StickyInMemoryEngineID string
   173  	// UseFileRegistry is true if the "file registry" store version is desired.
   174  	// This is set by CCL code when encryption-at-rest is in use.
   175  	UseFileRegistry bool
   176  	// RocksDBOptions contains RocksDB specific options using a semicolon
   177  	// separated key-value syntax ("key1=value1; key2=value2").
   178  	RocksDBOptions string
   179  	// ExtraOptions is a serialized protobuf set by Go CCL code and passed through
   180  	// to C CCL code.
   181  	ExtraOptions []byte
   182  }
   183  
   184  // String returns a fully parsable version of the store spec.
   185  func (ss StoreSpec) String() string {
   186  	var buffer bytes.Buffer
   187  	if len(ss.Path) != 0 {
   188  		fmt.Fprintf(&buffer, "path=%s,", ss.Path)
   189  	}
   190  	if ss.InMemory {
   191  		fmt.Fprint(&buffer, "type=mem,")
   192  	}
   193  	if ss.Size.InBytes > 0 {
   194  		fmt.Fprintf(&buffer, "size=%s,", humanizeutil.IBytes(ss.Size.InBytes))
   195  	}
   196  	if ss.Size.Percent > 0 {
   197  		fmt.Fprintf(&buffer, "size=%s%%,", humanize.Ftoa(ss.Size.Percent))
   198  	}
   199  	if len(ss.Attributes.Attrs) > 0 {
   200  		fmt.Fprint(&buffer, "attrs=")
   201  		for i, attr := range ss.Attributes.Attrs {
   202  			if i != 0 {
   203  				fmt.Fprint(&buffer, ":")
   204  			}
   205  			buffer.WriteString(attr)
   206  		}
   207  		fmt.Fprintf(&buffer, ",")
   208  	}
   209  	// Trim the extra comma from the end if it exists.
   210  	if l := buffer.Len(); l > 0 {
   211  		buffer.Truncate(l - 1)
   212  	}
   213  	return buffer.String()
   214  }
   215  
   216  // fractionRegex is the regular expression that recognizes whether
   217  // the specified size is a fraction of the total available space.
   218  // Proportional sizes can be expressed as fractional numbers, either
   219  // in absolute value or with a trailing "%" sign. A fractional number
   220  // without a trailing "%" must be recognized by the presence of a
   221  // decimal separator; numbers without decimal separators are plain
   222  // sizes in bytes (separate case in the parsing).
   223  // The first part of the regexp matches NNN.[MMM]; the second part
   224  // [NNN].MMM, and the last part matches explicit percentages with or
   225  // without a decimal separator.
   226  // Values smaller than 1% and 100% are rejected after parsing using
   227  // a separate check.
   228  var fractionRegex = regexp.MustCompile(`^([-]?([0-9]+\.[0-9]*|[0-9]*\.[0-9]+|[0-9]+(\.[0-9]*)?%))$`)
   229  
   230  // NewStoreSpec parses the string passed into a --store flag and returns a
   231  // StoreSpec if it is correctly parsed.
   232  // There are four possible fields that can be passed in, comma separated:
   233  // - path=xxx The directory in which to the rocks db instance should be
   234  //   located, required unless using a in memory storage.
   235  // - type=mem This specifies that the store is an in memory storage instead of
   236  //   an on disk one. mem is currently the only other type available.
   237  // - size=xxx The optional maximum size of the storage. This can be in one of a
   238  //   few different formats.
   239  //   - 10000000000     -> 10000000000 bytes
   240  //   - 20GB            -> 20000000000 bytes
   241  //   - 20GiB           -> 21474836480 bytes
   242  //   - 0.02TiB         -> 21474836480 bytes
   243  //   - 20%             -> 20% of the available space
   244  //   - 0.2             -> 20% of the available space
   245  // - attrs=xxx:yyy:zzz A colon separated list of optional attributes.
   246  // Note that commas are forbidden within any field name or value.
   247  func NewStoreSpec(value string) (StoreSpec, error) {
   248  	const pathField = "path"
   249  	if len(value) == 0 {
   250  		return StoreSpec{}, fmt.Errorf("no value specified")
   251  	}
   252  	var ss StoreSpec
   253  	used := make(map[string]struct{})
   254  	for _, split := range strings.Split(value, ",") {
   255  		if len(split) == 0 {
   256  			continue
   257  		}
   258  		subSplits := strings.SplitN(split, "=", 2)
   259  		var field string
   260  		var value string
   261  		if len(subSplits) == 1 {
   262  			field = pathField
   263  			value = subSplits[0]
   264  		} else {
   265  			field = strings.ToLower(subSplits[0])
   266  			value = subSplits[1]
   267  		}
   268  		if _, ok := used[field]; ok {
   269  			return StoreSpec{}, fmt.Errorf("%s field was used twice in store definition", field)
   270  		}
   271  		used[field] = struct{}{}
   272  
   273  		if len(field) == 0 {
   274  			continue
   275  		}
   276  		if len(value) == 0 {
   277  			return StoreSpec{}, fmt.Errorf("no value specified for %s", field)
   278  		}
   279  
   280  		switch field {
   281  		case pathField:
   282  			var err error
   283  			ss.Path, err = GetAbsoluteStorePath(pathField, value)
   284  			if err != nil {
   285  				return StoreSpec{}, err
   286  			}
   287  		case "size":
   288  			var err error
   289  			var minBytesAllowed int64 = MinimumStoreSize
   290  			var minPercent float64 = 1
   291  			var maxPercent float64 = 100
   292  			ss.Size, err = NewSizeSpec(
   293  				value,
   294  				&intInterval{min: &minBytesAllowed},
   295  				&floatInterval{min: &minPercent, max: &maxPercent},
   296  			)
   297  			if err != nil {
   298  				return StoreSpec{}, err
   299  			}
   300  		case "attrs":
   301  			// Check to make sure there are no duplicate attributes.
   302  			attrMap := make(map[string]struct{})
   303  			for _, attribute := range strings.Split(value, ":") {
   304  				if _, ok := attrMap[attribute]; ok {
   305  					return StoreSpec{}, fmt.Errorf("duplicate attribute given for store: %s", attribute)
   306  				}
   307  				attrMap[attribute] = struct{}{}
   308  			}
   309  			for attribute := range attrMap {
   310  				ss.Attributes.Attrs = append(ss.Attributes.Attrs, attribute)
   311  			}
   312  			sort.Strings(ss.Attributes.Attrs)
   313  		case "type":
   314  			if value == "mem" {
   315  				ss.InMemory = true
   316  			} else {
   317  				return StoreSpec{}, fmt.Errorf("%s is not a valid store type", value)
   318  			}
   319  		case "rocksdb":
   320  			ss.RocksDBOptions = value
   321  		default:
   322  			return StoreSpec{}, fmt.Errorf("%s is not a valid store field", field)
   323  		}
   324  	}
   325  	if ss.InMemory {
   326  		// Only in memory stores don't need a path and require a size.
   327  		if ss.Path != "" {
   328  			return StoreSpec{}, fmt.Errorf("path specified for in memory store")
   329  		}
   330  		if ss.Size.Percent == 0 && ss.Size.InBytes == 0 {
   331  			return StoreSpec{}, fmt.Errorf("size must be specified for an in memory store")
   332  		}
   333  	} else if ss.Path == "" {
   334  		return StoreSpec{}, fmt.Errorf("no path specified")
   335  	}
   336  	return ss, nil
   337  }
   338  
   339  // StoreSpecList contains a slice of StoreSpecs that implements pflag's value
   340  // interface.
   341  type StoreSpecList struct {
   342  	Specs   []StoreSpec
   343  	updated bool // updated is used to determine if specs only contain the default value.
   344  }
   345  
   346  var _ pflag.Value = &StoreSpecList{}
   347  
   348  // String returns a string representation of all the StoreSpecs. This is part
   349  // of pflag's value interface.
   350  func (ssl StoreSpecList) String() string {
   351  	var buffer bytes.Buffer
   352  	for _, ss := range ssl.Specs {
   353  		fmt.Fprintf(&buffer, "--%s=%s ", cliflags.Store.Name, ss)
   354  	}
   355  	// Trim the extra space from the end if it exists.
   356  	if l := buffer.Len(); l > 0 {
   357  		buffer.Truncate(l - 1)
   358  	}
   359  	return buffer.String()
   360  }
   361  
   362  // AuxiliaryDir is the path of the auxiliary dir relative to an engine.Engine's
   363  // root directory. It must not be changed without a proper migration.
   364  const AuxiliaryDir = "auxiliary"
   365  
   366  // PreventedStartupFile is the filename (relative to 'dir') used for files that
   367  // can block server startup.
   368  func PreventedStartupFile(dir string) string {
   369  	return filepath.Join(dir, "_CRITICAL_ALERT.txt")
   370  }
   371  
   372  // PriorCriticalAlertError attempts to read the
   373  // PreventedStartupFile for each store directory and returns their
   374  // contents as a structured error.
   375  //
   376  // These files typically request operator intervention after a
   377  // corruption event by preventing the affected node(s) from starting
   378  // back up.
   379  func (ssl StoreSpecList) PriorCriticalAlertError() (err error) {
   380  	addError := func(newErr error) {
   381  		if err == nil {
   382  			err = errors.New("startup forbidden by prior critical alert")
   383  		}
   384  		// We use WithDetailf here instead of errors.CombineErrors
   385  		// because we want the details to be printed to the screen
   386  		// (combined errors only show up via %+v).
   387  		err = errors.WithDetailf(err, "%v", newErr)
   388  	}
   389  	for _, ss := range ssl.Specs {
   390  		path := ss.PreventedStartupFile()
   391  		if path == "" {
   392  			continue
   393  		}
   394  		b, err := ioutil.ReadFile(path)
   395  		if err != nil {
   396  			if !os.IsNotExist(err) {
   397  				addError(errors.Wrapf(err, "%s", path))
   398  			}
   399  			continue
   400  		}
   401  		addError(errors.Newf("From %s:\n\n%s\n", path, b))
   402  	}
   403  	return err
   404  }
   405  
   406  // PreventedStartupFile returns the path to a file which, if it exists, should
   407  // prevent the server from starting up. Returns an empty string for in-memory
   408  // engines.
   409  func (ss StoreSpec) PreventedStartupFile() string {
   410  	if ss.InMemory {
   411  		return ""
   412  	}
   413  	return PreventedStartupFile(filepath.Join(ss.Path, AuxiliaryDir))
   414  }
   415  
   416  // Type returns the underlying type in string form. This is part of pflag's
   417  // value interface.
   418  func (ssl *StoreSpecList) Type() string {
   419  	return "StoreSpec"
   420  }
   421  
   422  // Set adds a new value to the StoreSpecValue. It is the important part of
   423  // pflag's value interface.
   424  func (ssl *StoreSpecList) Set(value string) error {
   425  	spec, err := NewStoreSpec(value)
   426  	if err != nil {
   427  		return err
   428  	}
   429  	if !ssl.updated {
   430  		ssl.Specs = []StoreSpec{spec}
   431  		ssl.updated = true
   432  	} else {
   433  		ssl.Specs = append(ssl.Specs, spec)
   434  	}
   435  	return nil
   436  }
   437  
   438  // JoinListType is a slice of strings that implements pflag's value
   439  // interface.
   440  type JoinListType []string
   441  
   442  // String returns a string representation of all the JoinListType. This is part
   443  // of pflag's value interface.
   444  func (jls JoinListType) String() string {
   445  	var buffer bytes.Buffer
   446  	for _, jl := range jls {
   447  		fmt.Fprintf(&buffer, "--join=%s ", jl)
   448  	}
   449  	// Trim the extra space from the end if it exists.
   450  	if l := buffer.Len(); l > 0 {
   451  		buffer.Truncate(l - 1)
   452  	}
   453  	return buffer.String()
   454  }
   455  
   456  // Type returns the underlying type in string form. This is part of pflag's
   457  // value interface.
   458  func (jls *JoinListType) Type() string {
   459  	return "string"
   460  }
   461  
   462  // Set adds a new value to the JoinListType. It is the important part of
   463  // pflag's value interface.
   464  func (jls *JoinListType) Set(value string) error {
   465  	if strings.TrimSpace(value) == "" {
   466  		// No value, likely user error.
   467  		return errors.New("no address specified in --join")
   468  	}
   469  	for _, v := range strings.Split(value, ",") {
   470  		v = strings.TrimSpace(v)
   471  		if v == "" {
   472  			// --join=a,,b  equivalent to --join=a,b
   473  			continue
   474  		}
   475  		// Try splitting the address. This validates the format
   476  		// of the address and tolerates a missing delimiter colon
   477  		// between the address and port number.
   478  		addr, port, err := netutil.SplitHostPort(v, "")
   479  		if err != nil {
   480  			return err
   481  		}
   482  		// Re-join the parts. This guarantees an address that
   483  		// will be valid for net.SplitHostPort().
   484  		*jls = append(*jls, net.JoinHostPort(addr, port))
   485  	}
   486  	return nil
   487  }