github.com/grailbio/base@v0.0.11/config/profile.go (about) 1 // Copyright 2019 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache 2.0 3 // license that can be found in the LICENSE file. 4 5 // Package config is used to configure software systems. A 6 // configuration managed by package config is called a profile. The 7 // binary that loads a profile declares a set of named, global 8 // objects through the APIs in this package. A profile configures 9 // these objects (objects may depend on each other, forming a DAG) 10 // and lets the user retrieve configured objects through its API. 11 // 12 // The semantics of profiles provide the kind of flexibility that is 13 // often required in operational contexts. Profiles define a 14 // principled overriding so that a base configuration can be extended 15 // by the user, either by composing multiple configuration or by 16 // editing the configuration through a command-line integration. 17 // Profiles may also derive multiple instances from the same base 18 // instance in order to provide small variations on instance 19 // configuration. Profiles define a concrete syntax so that they may 20 // be stored (e.g., centrally) or transmitted over a network 21 // connection (e.g., to bootstrap a remote binary with a particular 22 // configuration). Profiles are also self-documenting in the manner 23 // of Go's flag package. Profiles are resolved lazily, and thus 24 // maintain configuration for unknown instances, so long as these are 25 // never retrieved. This permits a single profile to be reused across 26 // many binaries without concern for compatibility. 27 // 28 // Profile syntax 29 // 30 // A profile contains a set of clauses, or directives. Each clause 31 // either declares a new instance or configures an existing instance. 32 // Clauses are interpreted in order, top-to-bottom, and later 33 // configurations override earlier configurations. These semantics 34 // accommodate for "overlays", where for example a user profile is 35 // loaded after a base profile to provide customization. Within 36 // GRAIL, a base profile is declared in the standard package 37 // github.com/grailbio/base/grail, which also loads a user 38 // profile from $HOME/grail/profile. 39 // 40 // A parameter is set by the directive param. For example, the 41 // following sets the parallelism parameter on the instance bigslice 42 // to 1024: 43 // 44 // param bigslice parallelism = 1024 45 // 46 // The values supported by profiles are: integers, strings, booleans, floats, 47 // and indirections (naming other instances). The following shows an example 48 // of each: 49 // 50 // param bigslice load-factor = 0.8 51 // param bigmachine/ec2system username = "marius" 52 // param bigmachine/ec2system on-demand = false 53 // param s3 retries = 8 54 // 55 // As a shortcut, parameters for the same instance may be grouped 56 // together. For example, the two parameters on the instance 57 // bigmachine/ec2system may be grouped together as follows: 58 // 59 // param bigmachine/ec2system ( 60 // username = "marius" 61 // on-demand = false 62 // ) 63 // 64 // Instances may refer to each other by name. The following 65 // configures the aws/ticket instance to use a particular ticket path 66 // and region; it then configures bigmachine/ec2system to use this 67 // AWS session. 68 // 69 // param aws/ticket ( 70 // path = "eng/dev/aws" 71 // region = "us-west-2" 72 // ) 73 // 74 // param bigmachine/ec2system aws = aws/ticket 75 // 76 // Profiles may also define new instances with different configurations. 77 // This is done via the instance directive. For example, if we wanted to 78 // declare a new bigmachine/ec2system that used on-demand instances 79 // instead of spot instances, we could define a profile as follows: 80 // 81 // instance bigmachine/ec2ondemand bigmachine/ec2system 82 // 83 // param bigmachine/ec2ondemand on-demand = false 84 // 85 // Since it is common to declare an instance and configure it, the 86 // profile syntax provides an affordance for combining the two, 87 // also through grouping. The above is equivalent to: 88 // 89 // instance bigmachine/ec2ondemand bigmachine/ec2system ( 90 // on-demand = false 91 // username = "marius-ondemand" 92 // // (any other configuration to be changed from the base) 93 // ) 94 // 95 // New instances may depend on any instance. For example, the above 96 // may be further customized as follows. 97 // 98 // instance bigmachine/ec2ondemand-anonymous bigmachine/ec2ondemand ( 99 // username = "anonymous" 100 // ) 101 // 102 // Customization through flags 103 // 104 // Profile parameters may be adjusted via command-line flags. Profile 105 // provides utility methods to register flags and interpret them. See 106 // the appropriate methods for more details. Any parameter may be 107 // set through the provided command-line flags by specifying the path 108 // to the parameter. As an example, the following invocations customize 109 // aspects of the above profile. 110 // 111 // # Override the ticket path and the default ec2system username. 112 // # -set flags are interpreted in order, and the following is equivalent 113 // # to the clauses 114 // # param aws/ticket path = "eng/prod/aws" 115 // # param bigmachine/ec2system username = "anonymous" 116 // $ program -set aws/ticket.path=eng/prod/aws -set bigmachine/ec2system.username=anonymous 117 // 118 // # User the aws/env instance instead of aws/ticket, as above. 119 // # The type of a flag is interpreted based on underlying type, so 120 // # the following is equivalent to the clause 121 // # param bigmachine/ec2system aws = aws/env 122 // $ program -set bigmachine/ec2system.aws=aws/env 123 // 124 // Default profile 125 // 126 // Package config also defines a default profile and a set of package-level 127 // methods that operate on this profile. Most users should make use only 128 // of the default profile. This package also exports an http handler on the 129 // path /debug/profile on the default (global) ServeMux, which returns the 130 // global profile in parseable form. 131 package config 132 133 import ( 134 "fmt" 135 "io" 136 "log" 137 "net/http" 138 "reflect" 139 "runtime" 140 "sort" 141 "strconv" 142 "strings" 143 "sync" 144 "unsafe" 145 ) 146 147 func init() { 148 http.HandleFunc("/debug/profile", func(w http.ResponseWriter, r *http.Request) { 149 if err := Application().PrintTo(w); err != nil { 150 http.Error(w, fmt.Sprintf("writing profile: %v", err), http.StatusInternalServerError) 151 } 152 }) 153 } 154 155 type ( 156 // Profile stores a set of parameters and configures instances based 157 // on these. It is the central data structure of this package as 158 // detailed in the package docs. Each Profile instance maintains its 159 // own set of instances. Most users should use the package-level 160 // methods that operate on the default profile. 161 Profile struct { 162 // The following are used by the flag registration and 163 // handling mechanism. 164 flags flags 165 flagDump bool 166 167 globals map[string]typedConstructor 168 169 mu sync.Mutex 170 instances instances 171 cached map[string]interface{} 172 } 173 // typedConstructor is a type-erased *Constructor[T]. 174 typedConstructor struct { 175 constructor *Constructor[any] 176 typ reflect.Type 177 } 178 ) 179 180 // New creates and returns a new profile, installing all currently 181 // registered global objects. Global objects registered after a call 182 // to New are not reflected in the returned profile. 183 func New() *Profile { 184 p := &Profile{ 185 globals: make(map[string]typedConstructor), 186 instances: make(instances), 187 cached: make(map[string]interface{}), 188 } 189 190 globalsMu.Lock() 191 for name, ct := range globals { 192 p.globals[name] = typedConstructor{newConstructor(), ct.typ} 193 ct.configure(p.globals[name].constructor) 194 } 195 globalsMu.Unlock() 196 197 // Make a shadow instance for each global instance. This helps keep 198 // the downstream code simple. We also populate any defaults 199 // provided by the configured instances, so that printing the 200 // profile shows the true, global (and re-createable) state of the 201 // profile. 202 for name, global := range p.globals { 203 inst := &instance{name: name, params: make(map[string]interface{})} 204 for pname, param := range global.constructor.params { 205 // Special case for interface params: use their indirections 206 // instead of their value; this is always how they are satisfied 207 // in practice. 208 if param.kind == paramInterface { 209 inst.params[pname] = def{param.ifaceindir} 210 } else { 211 inst.params[pname] = def{param.Interface()} 212 } 213 } 214 p.instances[name] = inst 215 } 216 217 // Populate defaults as empty instance declarations, effectively 218 // redirecting the instance and making it overridable, etc. 219 globalsMu.Lock() 220 for name, parent := range defaults { 221 p.instances[name] = &instance{name: name, parent: parent} 222 } 223 globalsMu.Unlock() 224 225 return p 226 } 227 228 // Set sets the value of the parameter at the provided path to the 229 // provided value, which is intepreted according to the type of the 230 // parameter at that path. Set returns an error if the parameter does 231 // not exist or if the value cannot be parsed into the expected type. 232 // The path is a set of identifiers separated by dots ("."). Paths may 233 // traverse multiple indirections. 234 func (p *Profile) Set(path string, value string) error { 235 p.mu.Lock() 236 defer p.mu.Unlock() 237 238 // Special case: toplevel instance assignment. 239 elems := strings.Split(path, ".") 240 if len(elems) == 1 { 241 if value == "" || value == "nil" { 242 return fmt.Errorf( 243 "%s: top-level path may only be set to an instance; cannot be set to nil/empty", 244 elems[0], 245 ) 246 } 247 p.instances[elems[0]] = &instance{ 248 name: elems[0], 249 parent: value, 250 } 251 return nil 252 } 253 254 // Otherwise infer the type and parse it accordingly. 255 inst := p.instances[elems[0]] 256 if inst == nil { 257 return fmt.Errorf("%s: path not found: instance not found", path) 258 } 259 for i := 1; i < len(elems)-1; i++ { 260 var v interface{} 261 for { 262 var ok bool 263 v, ok = inst.params[elems[i]] 264 if ok { 265 break 266 } 267 if inst.parent == "" || p.instances[inst.parent] == nil { 268 return fmt.Errorf("%s: path not found: instance not found: %s", path, strings.Join(elems[:i], ".")) 269 } 270 inst = p.instances[inst.parent] 271 } 272 v, _ = unwrap(v) 273 indir, ok := v.(indirect) 274 if !ok { 275 return fmt.Errorf("%s: path not found: %s is not an instance", path, strings.Join(elems[:i], ".")) 276 } 277 inst = p.instances[string(indir)] 278 if inst == nil { 279 return fmt.Errorf("%s: path not found: instance not found: %s", path, strings.Join(elems[:i], ".")) 280 } 281 } 282 283 name := elems[len(elems)-1] 284 for { 285 if _, ok := inst.params[name]; ok { 286 break 287 } 288 if inst.parent == "" || p.instances[inst.parent] == nil { 289 return fmt.Errorf("%s: no such parameter", path) 290 } 291 inst = p.instances[inst.parent] 292 } 293 294 switch v, _ := unwrap(inst.params[name]); v.(type) { 295 case indirect: 296 // TODO(marius): validate that it's a good identifier? 297 if value == "nil" { 298 value = "" 299 } 300 inst.params[name] = indirect(value) 301 case string: 302 inst.params[name] = value 303 case bool: 304 v, err := strconv.ParseBool(value) 305 if err != nil { 306 return fmt.Errorf("param %s is a bool, but could not parse %s into bool: %v", path, value, err) 307 } 308 inst.params[name] = v 309 case int: 310 v, err := strconv.ParseInt(value, 0, 64) 311 if err != nil { 312 return fmt.Errorf("param %s is an int, but could not parse %s into int: %v", path, value, err) 313 } 314 inst.params[name] = int(v) 315 case float64: 316 v, err := strconv.ParseFloat(value, 64) 317 if err != nil { 318 return fmt.Errorf("param %s is a float, but could not parse %s into float: %v", path, value, err) 319 } 320 inst.params[name] = v 321 default: 322 panic(fmt.Sprintf("%T", v)) 323 } 324 return nil 325 } 326 327 // Get returns the value of the configured parameter at the provided 328 // dot-separated path. 329 func (p *Profile) Get(path string) (value string, ok bool) { 330 p.mu.Lock() 331 defer p.mu.Unlock() 332 333 var ( 334 elems = strings.Split(path, ".") 335 inst = p.instances[elems[0]] 336 ) 337 if inst == nil { 338 return "", false 339 } 340 // Special case: toplevels are "set" only if they are inherited. 341 // We return only the first level of inheritance. 342 if len(elems) == 1 { 343 return inst.parent, inst.parent != "" 344 } 345 346 for i := 1; i < len(elems)-1; i++ { 347 elem := elems[i] 348 for inst != nil && inst.params[elem] == nil { 349 inst = p.instances[inst.parent] 350 } 351 if inst == nil { 352 return "", false 353 } 354 v, _ := unwrap(inst.params[elem]) 355 indir, ok := v.(indirect) 356 if !ok { 357 return "", false 358 } 359 inst = p.instances[string(indir)] 360 if inst == nil { 361 return "", false 362 } 363 } 364 365 for elem := elems[len(elems)-1]; inst != nil; inst = p.instances[inst.parent] { 366 if v, ok := inst.params[elem]; ok { 367 v, _ = unwrap(v) 368 return fmt.Sprintf("%#v", v), true 369 } 370 } 371 return "", false 372 } 373 374 // Merge merges the instance parameters in profile q into p, 375 // so that parameters defined in q override those in p. 376 func (p *Profile) Merge(q *Profile) { 377 defer lock(p, q)() 378 for _, inst := range q.instances { 379 p.instances.Merge(inst) 380 } 381 } 382 383 // Parse parses a profile from the provided reader into p. On 384 // success, the instances defined by the profile in src are merged into 385 // profile p. If the reader implements 386 // 387 // Name() string 388 // 389 // then the result of calling Name is used as a filename to provide 390 // positional information in errors. 391 func (p *Profile) Parse(r io.Reader) error { 392 insts, err := parse(r) 393 if err != nil { 394 return err 395 } 396 p.mu.Lock() 397 defer p.mu.Unlock() 398 for _, inst := range insts { 399 p.instances.Merge(inst) 400 } 401 return nil 402 } 403 404 // InstanceNames returns the set of names of instances provided by p. 405 func (p *Profile) InstanceNames() map[string]struct{} { 406 p.mu.Lock() 407 defer p.mu.Unlock() 408 names := make(map[string]struct{}, len(p.instances)) 409 for name := range p.instances { 410 names[name] = struct{}{} 411 } 412 return names 413 } 414 415 // Instance retrieves the named instance from this profile into the 416 // pointer ptr. All of its parameters are fully resolved and the 417 // underlying global object is instantiated according to the desired 418 // parameterization. Instance panics if ptr is not a pointer type. If the 419 // type of the instance cannot be assigned to the value pointed to by 420 // ptr, an error is returned. Since such errors may occur 421 // transitively (e.g., the type of an instance required by another 422 // instance may be wrong), the source location of the type mismatch 423 // is included in the error to help with debugging. Instances are 424 // cached and are only initialized the first time they are requested. 425 // 426 // If ptr is nil, the instance is created without populating the pointer. 427 func (p *Profile) Instance(name string, ptr interface{}) error { 428 var ptrv reflect.Value 429 if ptr != nil { 430 ptrv = reflect.ValueOf(ptr) 431 if ptrv.Kind() != reflect.Ptr { 432 panic("profile.Get: not a pointer") 433 } 434 } 435 _, file, line, _ := runtime.Caller(1) 436 p.mu.Lock() 437 err := p.getLocked(name, ptrv, file, line) 438 p.mu.Unlock() 439 return err 440 } 441 442 func (p *Profile) PrintTo(w io.Writer) error { 443 p.mu.Lock() 444 defer p.mu.Unlock() 445 instances := p.sorted() 446 for _, inst := range instances { 447 if len(inst.params) == 0 && inst.parent == "" { 448 continue 449 } 450 if _, err := fmt.Fprintln(w, inst.SyntaxString(p.docs(inst))); err != nil { 451 return err 452 } 453 } 454 return nil 455 } 456 457 // docs collects the documentation strings for inst and its parameters. 458 // Special key "" holds the documentation for the instance itself. 459 // Remaining keys are inst's param names. 460 func (p *Profile) docs(inst *instance) map[string]string { 461 global, ok := p.globals[inst.name] 462 if !ok { 463 return nil 464 } 465 docs := map[string]string{"": global.constructor.Doc} 466 for name, param := range global.constructor.params { 467 docs[name] = param.help 468 469 var paramType reflect.Type 470 if param.kind == paramInterface { 471 paramType = reflect.ValueOf(param.ifaceptr).Elem().Type() 472 } else { 473 paramType = reflect.TypeOf(param.Interface()) 474 } 475 var insts []string 476 // TODO: This is asymptotically slow. Make it faster, perhaps with indexing. 477 for name, other := range p.globals { 478 if name != inst.name && other.typ != nil && other.typ.AssignableTo(paramType) { 479 insts = append(insts, name) 480 } 481 } 482 sort.Strings(insts) 483 if len(insts) > 0 { 484 docs[name] += "\n\nAvailable instances:\n\t" + strings.Join(insts, "\n\t") 485 } 486 } 487 return docs 488 } 489 490 func (p *Profile) getLocked(name string, ptr reflect.Value, file string, line int) error { 491 if v, ok := p.cached[name]; ok { 492 return assign(name, v, ptr, file, line) 493 } 494 inst := p.instances[name] 495 if inst == nil { 496 return fmt.Errorf("no instance named %q", name) 497 } 498 499 resolved := make(map[string]interface{}) 500 for { 501 for k, v := range inst.params { 502 if _, ok := resolved[k]; !ok { 503 resolved[k] = v 504 } 505 } 506 if inst.parent == "" { 507 break 508 } 509 parent := p.instances[inst.parent] 510 if parent == nil { 511 return fmt.Errorf("no such instance: %q", inst.parent) 512 } 513 inst = parent 514 } 515 516 if _, ok := p.globals[inst.name]; !ok { 517 return fmt.Errorf("missing global instance: %q", inst.name) 518 } 519 // Even though we have a configured instance in globals, we create 520 // a new one to reduce the changes that multiple instances clobber 521 // each other. 522 globalsMu.Lock() 523 ct := globals[inst.name] 524 globalsMu.Unlock() 525 instance := newConstructor() 526 ct.configure(instance) 527 528 for pname, param := range instance.params { 529 val, ok := resolved[pname] 530 if !ok { 531 continue 532 } 533 // Skip defaults except for paramInterface since these need to be resolved. 534 if _, ok := val.(def); ok && param.kind != paramInterface { 535 continue 536 } 537 val, _ = unwrap(val) 538 if indir, ok := val.(indirect); ok { 539 if param.kind != paramInterface { 540 return fmt.Errorf("resolving %s.%s: cannot indirect parameters of type %T", name, pname, val) 541 } 542 if indir == "" { 543 typ := reflect.ValueOf(param.ifaceptr).Elem().Type() 544 if !isNilAssignable(typ) { 545 return fmt.Errorf("resolving %s.%s: cannot assign nil/empty to parameter of type %s", name, pname, typ) 546 } 547 continue // nil: skip 548 } 549 if err := p.getLocked(string(indir), reflect.ValueOf(param.ifaceptr), param.file, param.line); err != nil { 550 return err 551 } 552 continue 553 } 554 555 switch param.kind { 556 case paramInterface: 557 var ( 558 dst = reflect.ValueOf(param.ifaceptr).Elem() 559 src = reflect.ValueOf(val) 560 ) 561 // TODO: include embedded fields, etc? 562 if !src.Type().AssignableTo(dst.Type()) { 563 return fmt.Errorf("%s.%s: cannot assign value of type %s to type %s", name, pname, src.Type(), dst.Type()) 564 } 565 dst.Set(src) 566 case paramInt: 567 ival, ok := val.(int) 568 if !ok { 569 return fmt.Errorf("%s.%s: wrong parameter type: expected int, got %T", name, pname, val) 570 } 571 *param.intptr = ival 572 case paramFloat: 573 switch tv := val.(type) { 574 case int: 575 *param.floatptr = float64(tv) 576 case float64: 577 *param.floatptr = tv 578 default: 579 return fmt.Errorf("%s.%s: wrong parameter type: expected float64, got %T", name, pname, val) 580 } 581 case paramString: 582 sval, ok := val.(string) 583 if !ok { 584 return fmt.Errorf("%s.%s: wrong parameter type: expected string, got %T", name, pname, val) 585 } 586 *param.strptr = sval 587 case paramBool: 588 bval, ok := val.(bool) 589 if !ok { 590 return fmt.Errorf("%s.%s: wrong parameter type: expected bool, got %T", name, pname, val) 591 } 592 *param.boolptr = bval 593 default: 594 panic(param.kind) 595 } 596 } 597 598 v, err := instance.New() 599 if err != nil { 600 return err 601 } 602 p.cached[name] = v 603 return assign(name, v, ptr, file, line) 604 } 605 606 func (p *Profile) sorted() []*instance { 607 instances := make([]*instance, 0, len(p.instances)) 608 for _, inst := range p.instances { 609 instances = append(instances, inst) 610 } 611 sort.Slice(instances, func(i, j int) bool { 612 return instances[i].name < instances[j].name 613 }) 614 return instances 615 } 616 617 var ( 618 defaultInit sync.Once 619 defaultInstance *Profile 620 ) 621 622 // NewDefault is used to initialize the default profile. It can be 623 // set by a program before the application profile has been created 624 // in order to support asynchronous profile retrieval. 625 var NewDefault = New 626 627 // Application returns the default application profile. The default 628 // instance is initialized during the first call to Application (and thus 629 // of the package-level methods that operate on the default profile). 630 // Because of this, Application (and the other package-level methods 631 // operating on the default profile) should not be called during 632 // package initialization as doing so means that some global objects 633 // may not yet have been registered. 634 func Application() *Profile { 635 // TODO(marius): freeze registration after this? 636 defaultInit.Do(func() { 637 defaultInstance = NewDefault() 638 }) 639 return defaultInstance 640 } 641 642 // Merge merges profile p into the default profile. 643 // See Profile.Merge for more details. 644 func Merge(p *Profile) { 645 Application().Merge(p) 646 } 647 648 // Parse parses the profile in reader r into the default 649 // profile. See Profile.Parse for more details. 650 func Parse(r io.Reader) error { 651 return Application().Parse(r) 652 } 653 654 // Instance retrieves the instance with the provided name into the 655 // provided pointer from the default profile. See Profile.Instance for 656 // more details. 657 func Instance(name string, ptr interface{}) error { 658 return Application().Instance(name, ptr) 659 } 660 661 // Set sets the value of the parameter named by the provided path on 662 // the default profile. See Profile.Set for more details. 663 func Set(path, value string) error { 664 return Application().Set(path, value) 665 } 666 667 // Get retrieves the value of the parameter named by the provided path 668 // on the default profile. 669 func Get(path string) (value string, ok bool) { 670 return Application().Get(path) 671 } 672 673 // Must is a version of get which calls log.Fatal on error. 674 func Must(name string, ptr interface{}) { 675 if err := Instance(name, ptr); err != nil { 676 log.Fatal(err) 677 } 678 } 679 680 func assign(name string, instance interface{}, ptr reflect.Value, file string, line int) error { 681 if ptr == (reflect.Value{}) { 682 return nil 683 } 684 v := reflect.ValueOf(instance) 685 if !v.IsValid() { 686 ptr.Elem().Set(reflect.Zero(ptr.Elem().Type())) 687 return nil 688 } 689 if !v.Type().AssignableTo(ptr.Elem().Type()) { 690 return fmt.Errorf( 691 "%s:%d: instance %q of type %s is not assignable to provided pointer element type %s", 692 file, line, name, v.Type(), ptr.Elem().Type()) 693 } 694 ptr.Elem().Set(v) 695 return nil 696 } 697 698 func lock(p, q *Profile) (unlock func()) { 699 if uintptr(unsafe.Pointer(q)) < uintptr(unsafe.Pointer(p)) { 700 p, q = q, p 701 } 702 p.mu.Lock() 703 q.mu.Lock() 704 return func() { 705 q.mu.Unlock() 706 p.mu.Unlock() 707 } 708 }