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 }