github.com/m3db/m3@v1.5.0/src/integration/resources/inprocess/aggregator.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
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net"
    28  	"net/http"
    29  	"os"
    30  	"strconv"
    31  	"time"
    32  
    33  	"github.com/google/uuid"
    34  	"go.uber.org/zap"
    35  	"gopkg.in/yaml.v2"
    36  
    37  	m3agg "github.com/m3db/m3/src/aggregator/aggregator"
    38  	"github.com/m3db/m3/src/aggregator/server"
    39  	"github.com/m3db/m3/src/aggregator/tools/deploy"
    40  	"github.com/m3db/m3/src/cmd/services/m3aggregator/config"
    41  	"github.com/m3db/m3/src/integration/resources"
    42  	nettest "github.com/m3db/m3/src/integration/resources/net"
    43  	"github.com/m3db/m3/src/x/config/hostid"
    44  	xos "github.com/m3db/m3/src/x/os"
    45  )
    46  
    47  var errAggregatorNotStarted = errors.New("aggregator instance has not started")
    48  
    49  // Aggregator is an in-process implementation of resources.Aggregator for use
    50  // in integration tests.
    51  type Aggregator struct {
    52  	cfg     config.Configuration
    53  	logger  *zap.Logger
    54  	tmpDirs []string
    55  	startFn AggregatorStartFn
    56  
    57  	started    bool
    58  	httpClient deploy.AggregatorClient
    59  
    60  	interruptCh chan<- error
    61  	shutdownCh  <-chan struct{}
    62  }
    63  
    64  // AggregatorOptions are options of starting an in-process aggregator.
    65  type AggregatorOptions struct {
    66  	// Logger is the logger to use for the in-process aggregator.
    67  	Logger *zap.Logger
    68  	// StartFn is a custom function that can be used to start the Aggregator.
    69  	StartFn AggregatorStartFn
    70  	// Start indicates whether to start the aggregator instance
    71  	Start bool
    72  	// GeneratePorts will automatically update the config to use open ports
    73  	// if set to true. If false, configuration is used as-is re: ports.
    74  	GeneratePorts bool
    75  	// GenerateHostID will automatically update the host ID specified in
    76  	// the config if set to true. If false, configuration is used as-is re: host ID.
    77  	GenerateHostID bool
    78  }
    79  
    80  // NewAggregatorFromYAML creates a new in-process aggregator based on the yaml configuration
    81  // and options provided.
    82  func NewAggregatorFromYAML(yamlCfg string, opts AggregatorOptions) (resources.Aggregator, error) {
    83  	var cfg config.Configuration
    84  	if err := yaml.Unmarshal([]byte(yamlCfg), &cfg); err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	return NewAggregator(cfg, opts)
    89  }
    90  
    91  // NewAggregator creates a new in-process aggregator based on the configuration
    92  // and options provided.
    93  func NewAggregator(cfg config.Configuration, opts AggregatorOptions) (resources.Aggregator, error) {
    94  	cfg, tmpDirs, err := updateAggregatorConfig(cfg, opts)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// configure logger
   100  	hostID, err := cfg.AggregatorOrDefault().HostID.Resolve()
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	loggingCfg := cfg.LoggingOrDefault()
   106  	if len(loggingCfg.Fields) == 0 {
   107  		loggingCfg.Fields = make(map[string]interface{})
   108  	}
   109  	loggingCfg.Fields["component"] = fmt.Sprintf("m3aggregator:%s", hostID)
   110  
   111  	if opts.Logger == nil {
   112  		var err error
   113  		opts.Logger, err = resources.NewLogger()
   114  		if err != nil {
   115  			return nil, err
   116  		}
   117  	}
   118  
   119  	agg := &Aggregator{
   120  		cfg:        cfg,
   121  		logger:     opts.Logger,
   122  		tmpDirs:    tmpDirs,
   123  		startFn:    opts.StartFn,
   124  		started:    false,
   125  		httpClient: deploy.NewAggregatorClient(&http.Client{}),
   126  	}
   127  
   128  	if opts.Start {
   129  		agg.Start()
   130  	}
   131  
   132  	return agg, nil
   133  }
   134  
   135  // HostDetails returns the aggregator's host details.
   136  func (a *Aggregator) HostDetails() (*resources.InstanceInfo, error) {
   137  	id, err := a.cfg.AggregatorOrDefault().HostID.Resolve()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	addr, p, err := net.SplitHostPort(a.cfg.HTTPOrDefault().ListenAddress)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  
   147  	port, err := strconv.Atoi(p)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	m3msgAddr, m3msgP, err := net.SplitHostPort(a.cfg.M3MsgOrDefault().Server.ListenAddress)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	m3msgPort, err := strconv.Atoi(m3msgP)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	return &resources.InstanceInfo{
   163  		ID:           id,
   164  		Env:          a.cfg.KVClientOrDefault().Etcd.Env,
   165  		Zone:         a.cfg.KVClientOrDefault().Etcd.Zone,
   166  		Address:      addr,
   167  		Port:         uint32(port),
   168  		M3msgAddress: m3msgAddr,
   169  		M3msgPort:    uint32(m3msgPort),
   170  	}, nil
   171  }
   172  
   173  // Start starts the aggregator instance.
   174  //nolint:dupl
   175  func (a *Aggregator) Start() {
   176  	if a.started {
   177  		a.logger.Debug("aggregator instance has started already")
   178  		return
   179  	}
   180  	a.started = true
   181  
   182  	if a.startFn != nil {
   183  		a.interruptCh, a.shutdownCh = a.startFn(&a.cfg)
   184  		return
   185  	}
   186  
   187  	interruptCh := make(chan error, 1)
   188  	shutdownCh := make(chan struct{}, 1)
   189  
   190  	go func() {
   191  		server.Run(server.RunOptions{
   192  			Config:      a.cfg,
   193  			InterruptCh: interruptCh,
   194  			ShutdownCh:  shutdownCh,
   195  		})
   196  	}()
   197  
   198  	a.interruptCh = interruptCh
   199  	a.shutdownCh = shutdownCh
   200  }
   201  
   202  // IsHealthy determines whether an instance is healthy.
   203  func (a *Aggregator) IsHealthy() error {
   204  	if !a.started {
   205  		return errAggregatorNotStarted
   206  	}
   207  
   208  	return a.httpClient.IsHealthy(a.cfg.HTTPOrDefault().ListenAddress)
   209  }
   210  
   211  // Status returns the instance status.
   212  func (a *Aggregator) Status() (m3agg.RuntimeStatus, error) {
   213  	if !a.started {
   214  		return m3agg.RuntimeStatus{}, errAggregatorNotStarted
   215  	}
   216  
   217  	return a.httpClient.Status(a.cfg.HTTPOrDefault().ListenAddress)
   218  }
   219  
   220  // Resign asks an aggregator instance to give up its current leader role if applicable.
   221  func (a *Aggregator) Resign() error {
   222  	if !a.started {
   223  		return errAggregatorNotStarted
   224  	}
   225  
   226  	return a.httpClient.Resign(a.cfg.HTTPOrDefault().ListenAddress)
   227  }
   228  
   229  // Close closes the wrapper and releases any held resources, including
   230  // deleting docker containers.
   231  func (a *Aggregator) Close() error {
   232  	if !a.started {
   233  		return errAggregatorNotStarted
   234  	}
   235  
   236  	defer func() {
   237  		for _, dir := range a.tmpDirs {
   238  			if err := os.RemoveAll(dir); err != nil {
   239  				a.logger.Error("error removing temp directory", zap.String("dir", dir), zap.Error(err))
   240  			}
   241  		}
   242  		a.started = false
   243  	}()
   244  
   245  	select {
   246  	case a.interruptCh <- xos.NewInterruptError("in-process aggregator being shut down"):
   247  	case <-time.After(interruptTimeout):
   248  		return errors.New("timeout sending interrupt. closing without graceful shutdown")
   249  	}
   250  
   251  	select {
   252  	case <-a.shutdownCh:
   253  	case <-time.After(shutdownTimeout):
   254  		return errors.New("timeout waiting for shutdown notification. server closing may" +
   255  			" not be completely graceful")
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  // Configuration returns a copy of the configuration used to
   262  // start this aggregator.
   263  func (a *Aggregator) Configuration() config.Configuration {
   264  	return a.cfg
   265  }
   266  
   267  func updateAggregatorConfig(
   268  	cfg config.Configuration,
   269  	opts AggregatorOptions,
   270  ) (config.Configuration, []string, error) {
   271  	var (
   272  		tmpDirs []string
   273  		err     error
   274  	)
   275  
   276  	// Replace host ID with a config-based version.
   277  	if opts.GenerateHostID {
   278  		cfg = updateAggregatorHostID(cfg)
   279  	}
   280  
   281  	// Replace any ports with open ports
   282  	if opts.GeneratePorts {
   283  		cfg, err = updateAggregatorPorts(cfg)
   284  		if err != nil {
   285  			return config.Configuration{}, nil, err
   286  		}
   287  	}
   288  
   289  	// Replace any filepath with a temporary directory
   290  	cfg, tmpDirs, err = updateAggregatorFilepaths(cfg)
   291  	if err != nil {
   292  		return config.Configuration{}, nil, err
   293  	}
   294  
   295  	return cfg, tmpDirs, nil
   296  }
   297  
   298  func updateAggregatorHostID(cfg config.Configuration) config.Configuration {
   299  	hostID := uuid.New().String()
   300  	aggCfg := cfg.AggregatorOrDefault()
   301  	aggCfg.HostID = &hostid.Configuration{
   302  		Resolver: hostid.ConfigResolver,
   303  		Value:    &hostID,
   304  	}
   305  	cfg.Aggregator = &aggCfg
   306  
   307  	return cfg
   308  }
   309  
   310  func updateAggregatorPorts(cfg config.Configuration) (config.Configuration, error) {
   311  	httpCfg := cfg.HTTPOrDefault()
   312  	addr, _, err := nettest.GeneratePort(httpCfg.ListenAddress)
   313  	if err != nil {
   314  		return cfg, err
   315  	}
   316  	httpCfg.ListenAddress = addr
   317  	cfg.HTTP = &httpCfg
   318  
   319  	metricsCfg := cfg.MetricsOrDefault()
   320  	if metricsCfg.PrometheusReporter != nil && metricsCfg.PrometheusReporter.ListenAddress != "" {
   321  		addr, _, err := nettest.GeneratePort(metricsCfg.PrometheusReporter.ListenAddress)
   322  		if err != nil {
   323  			return cfg, err
   324  		}
   325  		promReporter := *metricsCfg.PrometheusReporter
   326  		promReporter.ListenAddress = addr
   327  		metricsCfg.PrometheusReporter = &promReporter
   328  	}
   329  	cfg.Metrics = &metricsCfg
   330  
   331  	m3msgCfg := cfg.M3MsgOrDefault()
   332  	if m3msgAddr := m3msgCfg.Server.ListenAddress; m3msgAddr != "" {
   333  		addr, _, err := nettest.GeneratePort(m3msgAddr)
   334  		if err != nil {
   335  			return cfg, err
   336  		}
   337  		m3msgCfg.Server.ListenAddress = addr
   338  	}
   339  	cfg.M3Msg = &m3msgCfg
   340  
   341  	return cfg, nil
   342  }
   343  
   344  func updateAggregatorFilepaths(cfg config.Configuration) (config.Configuration, []string, error) {
   345  	tmpDirs := make([]string, 0, 1)
   346  
   347  	kvCfg := cfg.KVClientOrDefault()
   348  	if kvCfg.Etcd != nil {
   349  		dir, err := ioutil.TempDir("", "m3agg-*")
   350  		if err != nil {
   351  			return cfg, tmpDirs, err
   352  		}
   353  		tmpDirs = append(tmpDirs, dir)
   354  		kvCfg.Etcd.CacheDir = dir
   355  	}
   356  	cfg.KVClient = &kvCfg
   357  
   358  	return cfg, tmpDirs, nil
   359  }