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 }