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