github.com/m3db/m3@v1.5.0/src/integration/resources/inprocess/coordinator.go (about) 1 // Copyright (c) 2021 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 // Package inprocess contains code for spinning up M3 resources in-process for 22 // the sake of integration testing. 23 package inprocess 24 25 import ( 26 "errors" 27 "fmt" 28 "io/ioutil" 29 "net" 30 "net/http" 31 "os" 32 "strconv" 33 "time" 34 35 "github.com/prometheus/common/model" 36 "go.uber.org/zap" 37 "gopkg.in/yaml.v2" 38 39 "github.com/m3db/m3/src/cmd/services/m3query/config" 40 "github.com/m3db/m3/src/integration/resources" 41 nettest "github.com/m3db/m3/src/integration/resources/net" 42 "github.com/m3db/m3/src/query/api/v1/options" 43 "github.com/m3db/m3/src/query/generated/proto/admin" 44 "github.com/m3db/m3/src/query/generated/proto/prompb" 45 "github.com/m3db/m3/src/query/server" 46 xconfig "github.com/m3db/m3/src/x/config" 47 "github.com/m3db/m3/src/x/headers" 48 xos "github.com/m3db/m3/src/x/os" 49 ) 50 51 const ( 52 interruptTimeout = 5 * time.Second 53 shutdownTimeout = time.Minute 54 ) 55 56 //nolint:maligned 57 // Coordinator is an in-process implementation of resources.Coordinator for use 58 // in integration tests. 59 type Coordinator struct { 60 cfg config.Configuration 61 client resources.CoordinatorClient 62 logger *zap.Logger 63 tmpDirs []string 64 embedded bool 65 startFn CoordinatorStartFn 66 started bool 67 68 interruptCh chan<- error 69 shutdownCh <-chan struct{} 70 } 71 72 //nolint:maligned 73 // CoordinatorOptions are options for starting a coordinator server. 74 type CoordinatorOptions struct { 75 // GeneratePorts will automatically update the config to use open ports 76 // if set to true. If false, configuration is used as-is re: ports. 77 GeneratePorts bool 78 // StartFn is a custom function that can be used to start the Coordinator. 79 StartFn CoordinatorStartFn 80 // Start indicates whether to start the coordinator instance. 81 Start bool 82 // Logger is the logger to use for the coordinator. If not provided, 83 // a default one will be created. 84 Logger *zap.Logger 85 } 86 87 // NewCoordinatorFromConfigFile creates a new in-process coordinator based on the config file 88 // and options provided. 89 func NewCoordinatorFromConfigFile(pathToCfg string, opts CoordinatorOptions) (resources.Coordinator, error) { 90 var cfg config.Configuration 91 if err := xconfig.LoadFile(&cfg, pathToCfg, xconfig.Options{}); err != nil { 92 return nil, err 93 } 94 95 return NewCoordinator(cfg, opts) 96 } 97 98 // NewCoordinatorFromYAML creates a new in-process coordinator based on the YAML configuration string 99 // and options provided. 100 func NewCoordinatorFromYAML(yamlCfg string, opts CoordinatorOptions) (resources.Coordinator, error) { 101 var cfg config.Configuration 102 if err := yaml.Unmarshal([]byte(yamlCfg), &cfg); err != nil { 103 return nil, err 104 } 105 106 return NewCoordinator(cfg, opts) 107 } 108 109 // NewCoordinator creates a new in-process coordinator based on the configuration 110 // and options provided. Use NewCoordinator or any of the convenience constructors 111 // (e.g. NewCoordinatorFromYAML, NewCoordinatorFromConfigFile) to get a running 112 // coordinator. 113 // 114 // The most typical usage of this method will be in an integration test to validate 115 // some behavior. For example, assuming we have a running DB node already, we could 116 // do the following to create a new namespace and write to it (note: ignoring error checking): 117 // 118 // coord, _ := NewCoordinatorFromYAML(defaultCoordConfig, CoordinatorOptions{}) 119 // coord.AddNamespace(admin.NamespaceAddRequest{...}) 120 // coord.WaitForNamespace(namespaceName) 121 // coord.WriteProm("cpu", map[string]string{"host", host}, samples) 122 // 123 // The coordinator will start up as you specify in your config. However, there is some 124 // helper logic to avoid port and filesystem collisions when spinning up multiple components 125 // within the process. If you specify a GeneratePorts: true in the CoordinatorOptions, address ports 126 // will be replaced with an open port. 127 // 128 // Similarly, filepath fields will be updated with a temp directory that will be cleaned up 129 // when the coordinator is destroyed. This should ensure that many of the same component can be 130 // spun up in-process without any issues with collisions. 131 func NewCoordinator(cfg config.Configuration, opts CoordinatorOptions) (resources.Coordinator, error) { 132 // Massage config so it runs properly in tests. 133 cfg, tmpDirs, err := updateCoordinatorConfig(cfg, opts) 134 if err != nil { 135 return nil, err 136 } 137 138 logging := cfg.LoggingOrDefault() 139 if len(logging.Fields) == 0 { 140 logging.Fields = make(map[string]interface{}) 141 } 142 logging.Fields["component"] = "coordinator" 143 cfg.Logging = &logging 144 145 // Configure logger 146 if opts.Logger == nil { 147 opts.Logger, err = resources.NewLogger() 148 if err != nil { 149 return nil, err 150 } 151 } 152 153 // Get HTTP port 154 _, p, err := net.SplitHostPort(cfg.ListenAddressOrDefault()) 155 if err != nil { 156 return nil, err 157 } 158 159 port, err := strconv.Atoi(p) 160 if err != nil { 161 return nil, err 162 } 163 164 // Start the coordinator 165 coord := &Coordinator{ 166 cfg: cfg, 167 client: resources.NewCoordinatorClient(resources.CoordinatorClientOptions{ 168 Client: &http.Client{}, 169 HTTPPort: port, 170 Logger: opts.Logger, 171 RetryFunc: resources.Retry, 172 }), 173 logger: opts.Logger, 174 tmpDirs: tmpDirs, 175 startFn: opts.StartFn, 176 } 177 if opts.Start { 178 coord.Start() 179 } 180 181 return coord, nil 182 } 183 184 // NewEmbeddedCoordinator creates a coordinator from one embedded within an existing 185 // db node. This method expects that the DB node has already been started before 186 // being called. 187 func NewEmbeddedCoordinator(d *DBNode) (resources.Coordinator, error) { 188 if !d.started { 189 return nil, errors.New("dbnode must be started to create the embedded coordinator") 190 } 191 192 _, p, err := net.SplitHostPort(d.cfg.Coordinator.ListenAddressOrDefault()) 193 if err != nil { 194 return nil, err 195 } 196 197 port, err := strconv.Atoi(p) 198 if err != nil { 199 return nil, err 200 } 201 202 return &Coordinator{ 203 cfg: *d.cfg.Coordinator, 204 client: resources.NewCoordinatorClient(resources.CoordinatorClientOptions{ 205 Client: &http.Client{}, 206 HTTPPort: port, 207 Logger: d.logger, 208 RetryFunc: resources.Retry, 209 }), 210 embedded: true, 211 logger: d.logger, 212 interruptCh: d.interruptCh, 213 shutdownCh: d.shutdownCh, 214 }, nil 215 } 216 217 // Start is the start method for the coordinator. 218 //nolint:dupl 219 func (c *Coordinator) Start() { 220 if c.started { 221 c.logger.Debug("coordinator already started") 222 return 223 } 224 c.started = true 225 226 if c.startFn != nil { 227 c.interruptCh, c.shutdownCh = c.startFn(&c.cfg) 228 return 229 } 230 231 interruptCh := make(chan error, 1) 232 shutdownCh := make(chan struct{}, 1) 233 234 go func() { 235 server.Run(server.RunOptions{ 236 Config: c.cfg, 237 InterruptCh: interruptCh, 238 ShutdownCh: shutdownCh, 239 }) 240 }() 241 242 c.interruptCh = interruptCh 243 c.shutdownCh = shutdownCh 244 } 245 246 // HostDetails returns the coordinator's host details. 247 func (c *Coordinator) HostDetails() (*resources.InstanceInfo, error) { 248 addr, p, err := net.SplitHostPort(c.cfg.ListenAddressOrDefault()) 249 if err != nil { 250 return nil, err 251 } 252 253 port, err := strconv.Atoi(p) 254 if err != nil { 255 return nil, err 256 } 257 258 var ( 259 m3msgAddr string 260 m3msgPort int 261 ) 262 if c.cfg.Ingest != nil { 263 a, p, err := net.SplitHostPort(c.cfg.Ingest.M3Msg.Server.ListenAddress) 264 if err != nil { 265 return nil, err 266 } 267 268 mp, err := strconv.Atoi(p) 269 if err != nil { 270 return nil, err 271 } 272 273 m3msgAddr, m3msgPort = a, mp 274 } 275 276 zone := headers.DefaultServiceZone 277 if len(c.cfg.Clusters) > 0 && c.cfg.Clusters[0].Client.EnvironmentConfig != nil { 278 envCfg := c.cfg.Clusters[0].Client.EnvironmentConfig 279 if len(envCfg.Services) > 0 && envCfg.Services[0].Service != nil { 280 zone = envCfg.Services[0].Service.Zone 281 } 282 } 283 284 return &resources.InstanceInfo{ 285 ID: "m3coordinator", 286 Zone: zone, 287 Address: addr, 288 Port: uint32(port), 289 M3msgAddress: m3msgAddr, 290 M3msgPort: uint32(m3msgPort), 291 }, nil 292 } 293 294 // GetNamespace gets namespaces. 295 func (c *Coordinator) GetNamespace() (admin.NamespaceGetResponse, error) { 296 return c.client.GetNamespace() 297 } 298 299 // WaitForNamespace blocks until the given namespace is enabled. 300 func (c *Coordinator) WaitForNamespace(name string) error { 301 return c.client.WaitForNamespace(name) 302 } 303 304 // AddNamespace adds a namespace. 305 func (c *Coordinator) AddNamespace(request admin.NamespaceAddRequest) (admin.NamespaceGetResponse, error) { 306 return c.client.AddNamespace(request) 307 } 308 309 // UpdateNamespace updates the namespace. 310 func (c *Coordinator) UpdateNamespace(request admin.NamespaceUpdateRequest) (admin.NamespaceGetResponse, error) { 311 return c.client.UpdateNamespace(request) 312 } 313 314 // DeleteNamespace removes the namespace. 315 func (c *Coordinator) DeleteNamespace(namespaceID string) error { 316 return c.client.DeleteNamespace(namespaceID) 317 } 318 319 // CreateDatabase creates a database. 320 func (c *Coordinator) CreateDatabase(request admin.DatabaseCreateRequest) (admin.DatabaseCreateResponse, error) { 321 return c.client.CreateDatabase(request) 322 } 323 324 // GetPlacement gets placements. 325 func (c *Coordinator) GetPlacement( 326 opts resources.PlacementRequestOptions, 327 ) (admin.PlacementGetResponse, error) { 328 return c.client.GetPlacement(opts) 329 } 330 331 // InitPlacement initializes placements. 332 func (c *Coordinator) InitPlacement( 333 opts resources.PlacementRequestOptions, 334 req admin.PlacementInitRequest, 335 ) (admin.PlacementGetResponse, error) { 336 return c.client.InitPlacement(opts, req) 337 } 338 339 // DeleteAllPlacements deletes all placements for the service specified 340 // in the PlacementRequestOptions. 341 func (c *Coordinator) DeleteAllPlacements( 342 opts resources.PlacementRequestOptions, 343 ) error { 344 return c.client.DeleteAllPlacements(opts) 345 } 346 347 // WaitForInstances blocks until the given instance is available. 348 func (c *Coordinator) WaitForInstances(ids []string) error { 349 return c.client.WaitForInstances(ids) 350 } 351 352 // WaitForShardsReady waits until all shards gets ready. 353 func (c *Coordinator) WaitForShardsReady() error { 354 return c.client.WaitForShardsReady() 355 } 356 357 // WaitForClusterReady waits until the cluster is ready to receive reads and writes. 358 func (c *Coordinator) WaitForClusterReady() error { 359 return c.client.WaitForClusterReady() 360 } 361 362 // Close closes the wrapper and releases any held resources, including 363 // deleting docker containers. 364 func (c *Coordinator) Close() error { 365 if c.embedded { 366 // NB(nate): for embedded coordinators, close is handled by the dbnode that 367 // it is spun up inside of. 368 return nil 369 } 370 371 defer func() { 372 for _, dir := range c.tmpDirs { 373 if err := os.RemoveAll(dir); err != nil { 374 c.logger.Error("error removing temp directory", zap.String("dir", dir), zap.Error(err)) 375 } 376 } 377 }() 378 379 select { 380 case c.interruptCh <- xos.NewInterruptError("in-process coordinator being shut down"): 381 case <-time.After(interruptTimeout): 382 return errors.New("timeout sending interrupt. closing without graceful shutdown") 383 } 384 385 select { 386 case <-c.shutdownCh: 387 case <-time.After(shutdownTimeout): 388 return errors.New("timeout waiting for shutdown notification. coordinator closing may" + 389 " not be completely graceful") 390 } 391 392 c.started = false 393 394 return nil 395 } 396 397 // InitM3msgTopic initializes an m3msg topic. 398 func (c *Coordinator) InitM3msgTopic( 399 opts resources.M3msgTopicOptions, 400 req admin.TopicInitRequest, 401 ) (admin.TopicGetResponse, error) { 402 return c.client.InitM3msgTopic(opts, req) 403 } 404 405 // GetM3msgTopic gets an m3msg topic. 406 func (c *Coordinator) GetM3msgTopic( 407 opts resources.M3msgTopicOptions, 408 ) (admin.TopicGetResponse, error) { 409 return c.client.GetM3msgTopic(opts) 410 } 411 412 // AddM3msgTopicConsumer adds a consumer service to an m3msg topic. 413 func (c *Coordinator) AddM3msgTopicConsumer( 414 opts resources.M3msgTopicOptions, 415 req admin.TopicAddRequest, 416 ) (admin.TopicGetResponse, error) { 417 return c.client.AddM3msgTopicConsumer(opts, req) 418 } 419 420 // ApplyKVUpdate applies a KV update. 421 func (c *Coordinator) ApplyKVUpdate(update string) error { 422 return c.client.ApplyKVUpdate(update) 423 } 424 425 // WriteCarbon writes a carbon metric datapoint at a given time. 426 func (c *Coordinator) WriteCarbon(port int, metric string, v float64, t time.Time) error { 427 return c.client.WriteCarbon(fmt.Sprintf("0.0.0.0:%d", port), metric, v, t) 428 } 429 430 // WriteProm writes a prometheus metric. Takes tags/labels as a map for convenience. 431 func (c *Coordinator) WriteProm( 432 name string, 433 tags map[string]string, 434 samples []prompb.Sample, 435 headers resources.Headers, 436 ) error { 437 return c.client.WriteProm(name, tags, samples, headers) 438 } 439 440 // WritePromWithRequest executes a prometheus write request. Allows you to 441 // provide the request directly which is useful for batch metric requests. 442 func (c *Coordinator) WritePromWithRequest(writeRequest prompb.WriteRequest, headers resources.Headers) error { 443 return c.client.WritePromWithRequest(writeRequest, headers) 444 } 445 446 // RunQuery runs the given query with a given verification function. 447 func (c *Coordinator) RunQuery( 448 verifier resources.ResponseVerifier, 449 query string, 450 headers resources.Headers, 451 ) error { 452 return c.client.RunQuery(verifier, query, headers) 453 } 454 455 // InstantQuery runs an instant query with provided headers 456 func (c *Coordinator) InstantQuery( 457 req resources.QueryRequest, 458 headers resources.Headers, 459 ) (model.Vector, error) { 460 return c.client.InstantQuery(req, headers) 461 } 462 463 // InstantQueryWithEngine runs an instant query with provided headers and the specified 464 // query engine. 465 func (c *Coordinator) InstantQueryWithEngine( 466 req resources.QueryRequest, 467 engine options.QueryEngine, 468 headers resources.Headers, 469 ) (model.Vector, error) { 470 return c.client.InstantQueryWithEngine(req, engine, headers) 471 } 472 473 // RangeQuery runs a range query with provided headers 474 func (c *Coordinator) RangeQuery( 475 req resources.RangeQueryRequest, 476 headers resources.Headers, 477 ) (model.Matrix, error) { 478 return c.client.RangeQuery(req, headers) 479 } 480 481 // GraphiteQuery retrieves graphite raw data. 482 func (c *Coordinator) GraphiteQuery(req resources.GraphiteQueryRequest) ([]resources.Datapoint, error) { 483 return c.client.GraphiteQuery(req) 484 } 485 486 // RangeQueryWithEngine runs a range query with provided headers and the specified 487 // query engine. 488 func (c *Coordinator) RangeQueryWithEngine( 489 req resources.RangeQueryRequest, 490 engine options.QueryEngine, 491 headers resources.Headers, 492 ) (model.Matrix, error) { 493 return c.client.RangeQueryWithEngine(req, engine, headers) 494 } 495 496 // LabelNames return matching label names based on the request. 497 func (c *Coordinator) LabelNames( 498 req resources.LabelNamesRequest, 499 headers resources.Headers, 500 ) (model.LabelNames, error) { 501 return c.client.LabelNames(req, headers) 502 } 503 504 // LabelValues returns matching label values based on the request. 505 func (c *Coordinator) LabelValues( 506 req resources.LabelValuesRequest, 507 headers resources.Headers, 508 ) (model.LabelValues, error) { 509 return c.client.LabelValues(req, headers) 510 } 511 512 // Series returns matching series based on the request. 513 func (c *Coordinator) Series( 514 req resources.SeriesRequest, 515 headers resources.Headers, 516 ) ([]model.Metric, error) { 517 return c.client.Series(req, headers) 518 } 519 520 // Configuration returns a copy of the configuration used to 521 // start this coordinator. 522 func (c *Coordinator) Configuration() config.Configuration { 523 return c.cfg 524 } 525 526 func updateCoordinatorConfig( 527 cfg config.Configuration, 528 opts CoordinatorOptions, 529 ) (config.Configuration, []string, error) { 530 var ( 531 tmpDirs []string 532 err error 533 ) 534 if opts.GeneratePorts { 535 // Replace any port with an open port 536 cfg, err = updateCoordinatorPorts(cfg) 537 if err != nil { 538 return config.Configuration{}, nil, err 539 } 540 } 541 542 // Replace any filepath with a temporary directory 543 cfg, tmpDirs, err = updateCoordinatorFilepaths(cfg) 544 if err != nil { 545 return config.Configuration{}, nil, err 546 } 547 548 return cfg, tmpDirs, nil 549 } 550 551 func updateCoordinatorPorts(cfg config.Configuration) (config.Configuration, error) { 552 addr, _, err := nettest.GeneratePort(cfg.ListenAddressOrDefault()) 553 if err != nil { 554 return cfg, err 555 } 556 cfg.ListenAddress = &addr 557 558 metrics := cfg.MetricsOrDefault() 559 if metrics.PrometheusReporter != nil && metrics.PrometheusReporter.ListenAddress != "" { 560 addr, _, err := nettest.GeneratePort(metrics.PrometheusReporter.ListenAddress) 561 if err != nil { 562 return cfg, err 563 } 564 metrics.PrometheusReporter.ListenAddress = addr 565 } 566 cfg.Metrics = metrics 567 568 if cfg.RPC != nil && cfg.RPC.ListenAddress != "" { 569 addr, _, err := nettest.GeneratePort(cfg.RPC.ListenAddress) 570 if err != nil { 571 return cfg, err 572 } 573 cfg.RPC.ListenAddress = addr 574 } 575 576 if cfg.Ingest != nil && cfg.Ingest.M3Msg.Server.ListenAddress != "" { 577 addr, _, err := nettest.GeneratePort(cfg.Ingest.M3Msg.Server.ListenAddress) 578 if err != nil { 579 return cfg, err 580 } 581 cfg.Ingest.M3Msg.Server.ListenAddress = addr 582 } 583 584 if cfg.Carbon != nil && cfg.Carbon.Ingester != nil { 585 addr, _, err := nettest.GeneratePort(cfg.Carbon.Ingester.ListenAddressOrDefault()) 586 if err != nil { 587 return cfg, err 588 } 589 cfg.Carbon.Ingester.ListenAddress = addr 590 } 591 592 return cfg, nil 593 } 594 595 func updateCoordinatorFilepaths(cfg config.Configuration) (config.Configuration, []string, error) { 596 tmpDirs := make([]string, 0, 1) 597 598 for _, cluster := range cfg.Clusters { 599 ec := cluster.Client.EnvironmentConfig 600 if ec != nil { 601 for _, svc := range ec.Services { 602 if svc != nil && svc.Service != nil { 603 dir, err := ioutil.TempDir("", "m3kv-*") 604 if err != nil { 605 return cfg, tmpDirs, err 606 } 607 608 tmpDirs = append(tmpDirs, dir) 609 svc.Service.CacheDir = dir 610 } 611 } 612 } 613 } 614 615 if cfg.ClusterManagement.Etcd != nil { 616 dir, err := ioutil.TempDir("", "m3kv-*") 617 if err != nil { 618 return cfg, tmpDirs, err 619 } 620 621 tmpDirs = append(tmpDirs, dir) 622 cfg.ClusterManagement.Etcd.CacheDir = dir 623 } 624 625 return cfg, tmpDirs, nil 626 }