github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/environment/config.go (about)

     1  // Copyright (c) 2017 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 environment
    22  
    23  import (
    24  	"errors"
    25  	"fmt"
    26  	"os"
    27  	"sort"
    28  	"time"
    29  
    30  	clusterclient "github.com/m3db/m3/src/cluster/client"
    31  	etcdclient "github.com/m3db/m3/src/cluster/client/etcd"
    32  	"github.com/m3db/m3/src/cluster/kv"
    33  	m3clusterkvmem "github.com/m3db/m3/src/cluster/kv/mem"
    34  	"github.com/m3db/m3/src/cluster/services"
    35  	"github.com/m3db/m3/src/cluster/shard"
    36  	"github.com/m3db/m3/src/dbnode/kvconfig"
    37  	"github.com/m3db/m3/src/dbnode/namespace"
    38  	"github.com/m3db/m3/src/dbnode/sharding"
    39  	"github.com/m3db/m3/src/dbnode/topology"
    40  	"github.com/m3db/m3/src/x/instrument"
    41  )
    42  
    43  var (
    44  	errInvalidConfig    = errors.New("must supply either service or static config")
    45  	errInvalidSyncCount = errors.New("must supply exactly one synchronous cluster")
    46  )
    47  
    48  // Configuration is a configuration that can be used to create namespaces, a topology, and kv store
    49  type Configuration struct {
    50  	// Services is used when a topology initializer is not supplied.
    51  	Services DynamicConfiguration `yaml:"services"`
    52  
    53  	// StaticConfiguration is used for running M3DB with static configs
    54  	Statics StaticConfiguration `yaml:"statics"`
    55  
    56  	// Presence of a (etcd) server in this config denotes an embedded cluster
    57  	SeedNodes *SeedNodesConfig `yaml:"seedNodes"`
    58  }
    59  
    60  // SeedNodesConfig defines fields for seed node
    61  type SeedNodesConfig struct {
    62  	RootDir                  string                 `yaml:"rootDir"`
    63  	InitialAdvertisePeerUrls []string               `yaml:"initialAdvertisePeerUrls"`
    64  	AdvertiseClientUrls      []string               `yaml:"advertiseClientUrls"`
    65  	ListenPeerUrls           []string               `yaml:"listenPeerUrls"`
    66  	ListenClientUrls         []string               `yaml:"listenClientUrls"`
    67  	InitialCluster           []SeedNode             `yaml:"initialCluster"`
    68  	ClientTransportSecurity  SeedNodeSecurityConfig `yaml:"clientTransportSecurity"`
    69  	PeerTransportSecurity    SeedNodeSecurityConfig `yaml:"peerTransportSecurity"`
    70  }
    71  
    72  // SeedNode represents a seed node for the cluster
    73  type SeedNode struct {
    74  	HostID       string `yaml:"hostID"`
    75  	Endpoint     string `yaml:"endpoint"`
    76  	ClusterState string `yaml:"clusterState"`
    77  }
    78  
    79  // SeedNodeSecurityConfig contains the data used for security in seed nodes
    80  type SeedNodeSecurityConfig struct {
    81  	CAFile        string `yaml:"caFile"`
    82  	CertFile      string `yaml:"certFile"`
    83  	KeyFile       string `yaml:"keyFile"`
    84  	TrustedCAFile string `yaml:"trustedCaFile"`
    85  	CertAuth      bool   `yaml:"clientCertAuth"`
    86  	AutoTLS       bool   `yaml:"autoTls"`
    87  }
    88  
    89  // DynamicConfiguration is used for running M3DB with a dynamic config
    90  type DynamicConfiguration []*DynamicCluster
    91  
    92  // DynamicCluster is a single cluster in a dynamic configuration
    93  type DynamicCluster struct {
    94  	Async           bool                      `yaml:"async"`
    95  	ClientOverrides ClientOverrides           `yaml:"clientOverrides"`
    96  	Service         *etcdclient.Configuration `yaml:"service"`
    97  }
    98  
    99  // ClientOverrides represents M3DB client overrides for a given cluster.
   100  type ClientOverrides struct {
   101  	HostQueueFlushInterval   *time.Duration `yaml:"hostQueueFlushInterval"`
   102  	TargetHostQueueFlushSize *int           `yaml:"targetHostQueueFlushSize"`
   103  }
   104  
   105  // Validate validates the DynamicConfiguration.
   106  func (c DynamicConfiguration) Validate() error {
   107  	syncCount := 0
   108  	for _, cfg := range c {
   109  		if !cfg.Async {
   110  			syncCount++
   111  		}
   112  		if cfg.ClientOverrides.TargetHostQueueFlushSize != nil && *cfg.ClientOverrides.TargetHostQueueFlushSize <= 1 {
   113  			return fmt.Errorf("target host queue flush size must be larger than zero but was: %d", cfg.ClientOverrides.TargetHostQueueFlushSize)
   114  		}
   115  
   116  		if cfg.ClientOverrides.HostQueueFlushInterval != nil && *cfg.ClientOverrides.HostQueueFlushInterval <= 0 {
   117  			return fmt.Errorf("host queue flush interval must be larger than zero but was: %s", cfg.ClientOverrides.HostQueueFlushInterval.String())
   118  		}
   119  	}
   120  	if syncCount != 1 {
   121  		return errInvalidSyncCount
   122  	}
   123  	return nil
   124  }
   125  
   126  // SyncCluster returns the synchronous cluster in the DynamicConfiguration
   127  func (c DynamicConfiguration) SyncCluster() (*DynamicCluster, error) {
   128  	if err := c.Validate(); err != nil {
   129  		return nil, err
   130  	}
   131  
   132  	for _, cluster := range c {
   133  		if !cluster.Async {
   134  			return cluster, nil
   135  		}
   136  	}
   137  	return nil, errInvalidSyncCount
   138  }
   139  
   140  // StaticConfiguration is used for running M3DB with a static config
   141  type StaticConfiguration []*StaticCluster
   142  
   143  // StaticCluster is a single cluster in a static configuration
   144  type StaticCluster struct {
   145  	Async           bool                              `yaml:"async"`
   146  	ClientOverrides ClientOverrides                   `yaml:"clientOverrides"`
   147  	Namespaces      []namespace.MetadataConfiguration `yaml:"namespaces"`
   148  	TopologyConfig  *topology.StaticConfiguration     `yaml:"topology"`
   149  	ListenAddress   string                            `yaml:"listenAddress"`
   150  }
   151  
   152  // Validate validates the StaticConfiguration
   153  func (c StaticConfiguration) Validate() error {
   154  	syncCount := 0
   155  	for _, cfg := range c {
   156  		if !cfg.Async {
   157  			syncCount++
   158  		}
   159  		if cfg.ClientOverrides.TargetHostQueueFlushSize != nil && *cfg.ClientOverrides.TargetHostQueueFlushSize <= 1 {
   160  			return fmt.Errorf("target host queue flush size must be larger than zero but was: %d", cfg.ClientOverrides.TargetHostQueueFlushSize)
   161  		}
   162  
   163  		if cfg.ClientOverrides.HostQueueFlushInterval != nil && *cfg.ClientOverrides.HostQueueFlushInterval <= 0 {
   164  			return fmt.Errorf("host queue flush interval must be larger than zero but was: %s", cfg.ClientOverrides.HostQueueFlushInterval.String())
   165  		}
   166  	}
   167  	if syncCount != 1 {
   168  		return errInvalidSyncCount
   169  	}
   170  	return nil
   171  }
   172  
   173  // ConfigureResult stores initializers and kv store for dynamic and static configs
   174  type ConfigureResult struct {
   175  	NamespaceInitializer namespace.Initializer
   176  	TopologyInitializer  topology.Initializer
   177  	ClusterClient        clusterclient.Client
   178  	KVStore              kv.Store
   179  	Async                bool
   180  	ClientOverrides      ClientOverrides
   181  }
   182  
   183  // ConfigureResults stores initializers and kv store for dynamic and static configs
   184  type ConfigureResults []ConfigureResult
   185  
   186  // SyncCluster returns the synchronous cluster in the ConfigureResults
   187  func (c ConfigureResults) SyncCluster() (ConfigureResult, error) {
   188  	for _, result := range c {
   189  		if !result.Async {
   190  			return result, nil
   191  		}
   192  	}
   193  	return ConfigureResult{}, errInvalidSyncCount
   194  }
   195  
   196  // ConfigurationParameters are options used to create new ConfigureResults
   197  type ConfigurationParameters struct {
   198  	InterruptedCh          <-chan struct{}
   199  	InstrumentOpts         instrument.Options
   200  	HashingSeed            uint32
   201  	HostID                 string
   202  	NewDirectoryMode       os.FileMode
   203  	ForceColdWritesEnabled bool
   204  	// AllowEmptyInitialNamespaceRegistry determines whether to allow the initial
   205  	// namespace update to be empty or to wait indefinitely until namespaces are received.
   206  	// This is used when configuring the namespaceInitializer.
   207  	AllowEmptyInitialNamespaceRegistry bool
   208  }
   209  
   210  // UnmarshalYAML normalizes the config into a list of services.
   211  func (c *Configuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
   212  	var cfg struct {
   213  		Services  DynamicConfiguration      `yaml:"services"`
   214  		Service   *etcdclient.Configuration `yaml:"service"`
   215  		Static    *StaticCluster            `yaml:"static"`
   216  		Statics   StaticConfiguration       `yaml:"statics"`
   217  		SeedNodes *SeedNodesConfig          `yaml:"seedNodes"`
   218  	}
   219  
   220  	if err := unmarshal(&cfg); err != nil {
   221  		return err
   222  	}
   223  
   224  	c.SeedNodes = cfg.SeedNodes
   225  	c.Statics = cfg.Statics
   226  	if cfg.Static != nil {
   227  		c.Statics = StaticConfiguration{cfg.Static}
   228  	}
   229  	c.Services = cfg.Services
   230  	if cfg.Service != nil {
   231  		c.Services = DynamicConfiguration{
   232  			&DynamicCluster{Service: cfg.Service},
   233  		}
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  // Validate validates the configuration.
   240  func (c *Configuration) Validate() error {
   241  	if (c.Services == nil && c.Statics == nil) ||
   242  		(len(c.Services) > 0 && len(c.Statics) > 0) {
   243  		return errInvalidConfig
   244  	}
   245  
   246  	if len(c.Services) > 0 {
   247  		if err := c.Services.Validate(); err != nil {
   248  			return err
   249  		}
   250  	}
   251  
   252  	if len(c.Statics) > 0 {
   253  		if err := c.Statics.Validate(); err != nil {
   254  			return err
   255  		}
   256  	}
   257  	return nil
   258  }
   259  
   260  // Configure creates a new ConfigureResults
   261  func (c Configuration) Configure(cfgParams ConfigurationParameters) (ConfigureResults, error) {
   262  	var emptyConfig ConfigureResults
   263  
   264  	// Validate here rather than UnmarshalYAML since we need to ensure one of
   265  	// dynamic or static configuration are provided. A blank YAML does not
   266  	// call UnmarshalYAML and therefore validation would be skipped.
   267  	if err := c.Validate(); err != nil {
   268  		return emptyConfig, err
   269  	}
   270  
   271  	if len(c.Services) > 0 {
   272  		return c.configureDynamic(cfgParams)
   273  	}
   274  
   275  	if len(c.Statics) > 0 {
   276  		return c.configureStatic(cfgParams)
   277  	}
   278  
   279  	return emptyConfig, errInvalidConfig
   280  }
   281  
   282  func (c Configuration) configureDynamic(cfgParams ConfigurationParameters) (ConfigureResults, error) {
   283  	var emptyConfig ConfigureResults
   284  	if err := c.Services.Validate(); err != nil {
   285  		return emptyConfig, err
   286  	}
   287  
   288  	cfgResults := make(ConfigureResults, 0, len(c.Services))
   289  	for _, cluster := range c.Services {
   290  		configSvcClientOpts := cluster.Service.NewOptions().
   291  			SetInstrumentOptions(cfgParams.InstrumentOpts).
   292  			// Set timeout to zero so it will wait indefinitely for the
   293  			// initial value.
   294  			SetServicesOptions(services.NewOptions().SetInitTimeout(0)).
   295  			SetNewDirectoryMode(cfgParams.NewDirectoryMode)
   296  		configSvcClient, err := etcdclient.NewConfigServiceClient(configSvcClientOpts)
   297  		if err != nil {
   298  			err = fmt.Errorf("could not create m3cluster client: %v", err)
   299  			return emptyConfig, err
   300  		}
   301  
   302  		dynamicOpts := namespace.NewDynamicOptions().
   303  			SetInstrumentOptions(cfgParams.InstrumentOpts).
   304  			SetConfigServiceClient(configSvcClient).
   305  			SetNamespaceRegistryKey(kvconfig.NamespacesKey).
   306  			SetForceColdWritesEnabled(cfgParams.ForceColdWritesEnabled).
   307  			SetAllowEmptyInitialNamespaceRegistry(cfgParams.AllowEmptyInitialNamespaceRegistry)
   308  		nsInit := namespace.NewDynamicInitializer(dynamicOpts)
   309  
   310  		serviceID := services.NewServiceID().
   311  			SetName(cluster.Service.Service).
   312  			SetEnvironment(cluster.Service.Env).
   313  			SetZone(cluster.Service.Zone)
   314  
   315  		topoOpts := topology.NewDynamicOptions().
   316  			SetConfigServiceClient(configSvcClient).
   317  			SetServiceID(serviceID).
   318  			SetQueryOptions(services.NewQueryOptions().
   319  				SetIncludeUnhealthy(true).
   320  				SetInterruptedCh(cfgParams.InterruptedCh)).
   321  			SetInstrumentOptions(cfgParams.InstrumentOpts).
   322  			SetHashGen(sharding.NewHashGenWithSeed(cfgParams.HashingSeed))
   323  		topoInit := topology.NewDynamicInitializer(topoOpts)
   324  
   325  		kv, err := configSvcClient.KV()
   326  		if err != nil {
   327  			err = fmt.Errorf("could not create KV client, %v", err)
   328  			return emptyConfig, err
   329  		}
   330  
   331  		result := ConfigureResult{
   332  			NamespaceInitializer: nsInit,
   333  			TopologyInitializer:  topoInit,
   334  			ClusterClient:        configSvcClient,
   335  			KVStore:              kv,
   336  			Async:                cluster.Async,
   337  			ClientOverrides:      cluster.ClientOverrides,
   338  		}
   339  		cfgResults = append(cfgResults, result)
   340  	}
   341  
   342  	return cfgResults, nil
   343  }
   344  
   345  func (c Configuration) configureStatic(cfgParams ConfigurationParameters) (ConfigureResults, error) {
   346  	var emptyConfig ConfigureResults
   347  
   348  	if err := c.Statics.Validate(); err != nil {
   349  		return emptyConfig, err
   350  	}
   351  
   352  	cfgResults := make(ConfigureResults, 0, len(c.Services))
   353  	for _, cluster := range c.Statics {
   354  		nsList := []namespace.Metadata{}
   355  		for _, ns := range cluster.Namespaces {
   356  			md, err := ns.Metadata()
   357  			if err != nil {
   358  				err = fmt.Errorf("unable to create metadata for static config: %v", err)
   359  				return emptyConfig, err
   360  			}
   361  			nsList = append(nsList, md)
   362  		}
   363  		// NB(bodu): Force cold writes to be enabled for all ns if specified.
   364  		if cfgParams.ForceColdWritesEnabled {
   365  			nsList = namespace.ForceColdWritesEnabledForMetadatas(nsList)
   366  		}
   367  
   368  		nsInitStatic := namespace.NewStaticInitializer(nsList)
   369  
   370  		numReplicas := cluster.TopologyConfig.Replicas
   371  		if numReplicas == 0 {
   372  			numReplicas = 1
   373  		}
   374  
   375  		shardSet, hostShardSets, err := newStaticShardSet(
   376  			cluster.TopologyConfig.Shards,
   377  			numReplicas,
   378  			cluster.TopologyConfig.Hosts,
   379  		)
   380  		if err != nil {
   381  			err = fmt.Errorf("unable to create shard set for static config: %v", err)
   382  			return emptyConfig, err
   383  		}
   384  		staticOptions := topology.NewStaticOptions().
   385  			SetHostShardSets(hostShardSets).
   386  			SetShardSet(shardSet).
   387  			SetReplicas(numReplicas)
   388  
   389  		topoInit := topology.NewStaticInitializer(staticOptions)
   390  		result := ConfigureResult{
   391  			NamespaceInitializer: nsInitStatic,
   392  			TopologyInitializer:  topoInit,
   393  			KVStore:              m3clusterkvmem.NewStore(),
   394  			Async:                cluster.Async,
   395  			ClientOverrides:      cluster.ClientOverrides,
   396  		}
   397  		cfgResults = append(cfgResults, result)
   398  	}
   399  
   400  	return cfgResults, nil
   401  }
   402  
   403  func newStaticShardSet(
   404  	numShards int,
   405  	rf int,
   406  	hosts []topology.HostShardConfig,
   407  ) (sharding.ShardSet, []topology.HostShardSet, error) {
   408  	var (
   409  		shardSet sharding.ShardSet
   410  		shardIDs []uint32
   411  		err      error
   412  	)
   413  
   414  	for i := uint32(0); i < uint32(numShards); i++ {
   415  		shardIDs = append(shardIDs, i)
   416  	}
   417  
   418  	shards := sharding.NewShards(shardIDs, shard.Available)
   419  	shardSet, err = sharding.NewShardSet(shards, sharding.DefaultHashFn(numShards))
   420  	if err != nil {
   421  		return nil, nil, err
   422  	}
   423  
   424  	hostShardSets, err := generatePlacement(hosts, numShards, rf)
   425  	if err != nil {
   426  		return nil, nil, err
   427  	}
   428  
   429  	return shardSet, hostShardSets, nil
   430  }
   431  
   432  func generatePlacement(hosts []topology.HostShardConfig, numShards int, rf int) ([]topology.HostShardSet, error) {
   433  	numHosts := len(hosts)
   434  	if numHosts == 0 || numShards < 1 || rf < 1 {
   435  		return nil, errors.New("number of hosts, shards, and RF must be positive")
   436  	}
   437  	if rf > numHosts {
   438  		return nil, errors.New("number of hosts must be >=RF")
   439  	}
   440  
   441  	hostShards := make([][]shard.Shard, numHosts)
   442  	hostIdx := 0
   443  	// Round robin assign shard replicas to hosts.
   444  	for shardInt := uint32(0); shardInt < uint32(numShards); shardInt++ {
   445  		for replica := 0; replica < rf; replica++ {
   446  			newShard := shard.NewShard(shardInt).SetState(shard.Available)
   447  			hostShards[hostIdx] = append(hostShards[hostIdx], newShard)
   448  			hostIdx = (hostIdx + 1) % numHosts
   449  		}
   450  	}
   451  
   452  	hostShardSets := make([]topology.HostShardSet, 0, numHosts)
   453  	sortedHosts := make([]topology.HostShardConfig, numHosts)
   454  	// Plain copy is okay because struct just contains strings.
   455  	copy(sortedHosts, hosts)
   456  	sort.Slice(sortedHosts, func(i, j int) bool { return sortedHosts[i].HostID < sortedHosts[j].HostID })
   457  	for i, host := range sortedHosts {
   458  		host := topology.NewHost(host.HostID, host.ListenAddress)
   459  		shardSet, err := sharding.NewShardSet(hostShards[i], sharding.DefaultHashFn(numShards))
   460  		if err != nil {
   461  			return nil, fmt.Errorf("error constructing new ShardSet: %w", err)
   462  		}
   463  		hostShardSet := topology.NewHostShardSet(host, shardSet)
   464  		hostShardSets = append(hostShardSets, hostShardSet)
   465  	}
   466  
   467  	return hostShardSets, nil
   468  }