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 }