github.com/m3db/m3@v1.5.0/src/query/api/v1/handler/database/create.go (about)

     1  // Copyright (c) 2018 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 database
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"math"
    28  	"net/http"
    29  	"strconv"
    30  	"strings"
    31  	"time"
    32  
    33  	clusterclient "github.com/m3db/m3/src/cluster/client"
    34  	"github.com/m3db/m3/src/cluster/generated/proto/placementpb"
    35  	clusterplacement "github.com/m3db/m3/src/cluster/placement"
    36  	"github.com/m3db/m3/src/cluster/placementhandler"
    37  	"github.com/m3db/m3/src/cluster/placementhandler/handleroptions"
    38  	dbconfig "github.com/m3db/m3/src/cmd/services/m3dbnode/config"
    39  	"github.com/m3db/m3/src/cmd/services/m3query/config"
    40  	dbnamespace "github.com/m3db/m3/src/dbnode/namespace"
    41  	"github.com/m3db/m3/src/query/api/v1/handler/namespace"
    42  	"github.com/m3db/m3/src/query/api/v1/options"
    43  	"github.com/m3db/m3/src/query/api/v1/route"
    44  	"github.com/m3db/m3/src/query/generated/proto/admin"
    45  	"github.com/m3db/m3/src/query/util/logging"
    46  	xerrors "github.com/m3db/m3/src/x/errors"
    47  	"github.com/m3db/m3/src/x/instrument"
    48  	xhttp "github.com/m3db/m3/src/x/net/http"
    49  
    50  	"github.com/gogo/protobuf/jsonpb"
    51  	"go.uber.org/zap"
    52  )
    53  
    54  const (
    55  	// CreateURL is the URL for the database create handler.
    56  	CreateURL = route.Prefix + "/database/create"
    57  
    58  	// CreateNamespaceURL is the URL for the database namespace create handler.
    59  	CreateNamespaceURL = route.Prefix + "/database/namespace/create"
    60  
    61  	// CreateHTTPMethod is the HTTP method used with the create database resource.
    62  	CreateHTTPMethod = http.MethodPost
    63  
    64  	// CreateNamespaceHTTPMethod is the HTTP method used with the create database namespace resource.
    65  	CreateNamespaceHTTPMethod = http.MethodPost
    66  
    67  	// DefaultLocalHostID is the default local host ID when creating a database.
    68  	DefaultLocalHostID = "m3db_local"
    69  
    70  	// DefaultLocalIsolationGroup is the default isolation group when creating a
    71  	// local database.
    72  	DefaultLocalIsolationGroup = "local"
    73  
    74  	// DefaultLocalZone is the default zone when creating a local database.
    75  	DefaultLocalZone = "embedded"
    76  
    77  	idealDatapointsPerBlock           = 720
    78  	blockSizeFromExpectedSeriesScalar = idealDatapointsPerBlock * int64(time.Hour)
    79  	shardMultiplier                   = 64
    80  
    81  	dbTypeLocal   dbType = "local"
    82  	dbTypeCluster dbType = "cluster"
    83  
    84  	defaultIsolationGroup = "local"
    85  	defaultZone           = "local"
    86  
    87  	defaultLocalRetentionPeriod = 24 * time.Hour
    88  
    89  	minRecommendCalculateBlockSize = 30 * time.Minute
    90  	maxRecommendCalculateBlockSize = 24 * time.Hour
    91  )
    92  
    93  type recommendedBlockSize struct {
    94  	forRetentionLessThanOrEqual time.Duration
    95  	blockSize                   time.Duration
    96  }
    97  
    98  var recommendedBlockSizesByRetentionAsc = []recommendedBlockSize{
    99  	{
   100  		forRetentionLessThanOrEqual: 12 * time.Hour,
   101  		blockSize:                   30 * time.Minute,
   102  	},
   103  	{
   104  		forRetentionLessThanOrEqual: 24 * time.Hour,
   105  		blockSize:                   time.Hour,
   106  	},
   107  	{
   108  		forRetentionLessThanOrEqual: 7 * 24 * time.Hour,
   109  		blockSize:                   2 * time.Hour,
   110  	},
   111  	{
   112  		forRetentionLessThanOrEqual: 30 * 24 * time.Hour,
   113  		blockSize:                   12 * time.Hour,
   114  	},
   115  	{
   116  		forRetentionLessThanOrEqual: 365 * 24 * time.Hour,
   117  		blockSize:                   24 * time.Hour,
   118  	},
   119  }
   120  
   121  var (
   122  	errMissingRequiredField    = xerrors.NewInvalidParamsError(errors.New("missing required field"))
   123  	errInvalidDBType           = xerrors.NewInvalidParamsError(errors.New("invalid database type"))
   124  	errMissingEmbeddedDBPort   = xerrors.NewInvalidParamsError(errors.New("unable to get port from embedded database listen address"))
   125  	errMissingEmbeddedDBConfig = xerrors.NewInvalidParamsError(errors.New("unable to find local embedded database config"))
   126  	errMissingHostID           = xerrors.NewInvalidParamsError(errors.New("missing host ID"))
   127  
   128  	errClusteredPlacementAlreadyExists        = xerrors.NewInvalidParamsError(errors.New("cannot use database create API to modify clustered placements after they are instantiated. Use the placement APIs directly to make placement changes, or remove the list of hosts from the request to add a namespace without modifying the placement"))
   129  	errCantReplaceLocalPlacementWithClustered = xerrors.NewInvalidParamsError(errors.New("cannot replace existing local placement with a clustered placement. Use the placement APIs directly to make placement changes, or remove the `type` field from the  request to add a namespace without modifying the existing local placement"))
   130  )
   131  
   132  type dbType string
   133  
   134  type createHandler struct {
   135  	placementInitHandler   *placementhandler.InitHandler
   136  	placementGetHandler    *placementhandler.GetHandler
   137  	namespaceAddHandler    *namespace.AddHandler
   138  	namespaceGetHandler    *namespace.GetHandler
   139  	namespaceDeleteHandler *namespace.DeleteHandler
   140  	embeddedDBCfg          *dbconfig.DBConfiguration
   141  	defaults               []handleroptions.ServiceOptionsDefault
   142  	instrumentOpts         instrument.Options
   143  }
   144  
   145  // NewCreateHandler returns a new instance of a database create handler.
   146  func NewCreateHandler(
   147  	client clusterclient.Client,
   148  	cfg config.Configuration,
   149  	embeddedDBCfg *dbconfig.DBConfiguration,
   150  	defaults []handleroptions.ServiceOptionsDefault,
   151  	instrumentOpts instrument.Options,
   152  	namespaceValidator options.NamespaceValidator,
   153  ) (http.Handler, error) {
   154  	placementHandlerOptions, err := placementhandler.NewHandlerOptions(client,
   155  		cfg.ClusterManagement.Placement, nil, instrumentOpts)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	return &createHandler{
   160  		placementInitHandler:   placementhandler.NewInitHandler(placementHandlerOptions),
   161  		placementGetHandler:    placementhandler.NewGetHandler(placementHandlerOptions),
   162  		namespaceAddHandler:    namespace.NewAddHandler(client, instrumentOpts, namespaceValidator),
   163  		namespaceGetHandler:    namespace.NewGetHandler(client, instrumentOpts),
   164  		namespaceDeleteHandler: namespace.NewDeleteHandler(client, instrumentOpts),
   165  		embeddedDBCfg:          embeddedDBCfg,
   166  		defaults:               defaults,
   167  		instrumentOpts:         instrumentOpts,
   168  	}, nil
   169  }
   170  
   171  func (h *createHandler) serviceNameAndDefaults() handleroptions.ServiceNameAndDefaults {
   172  	return handleroptions.ServiceNameAndDefaults{
   173  		ServiceName: handleroptions.M3DBServiceName,
   174  		Defaults:    h.defaults,
   175  	}
   176  }
   177  
   178  func (h *createHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   179  	var (
   180  		ctx    = r.Context()
   181  		logger = logging.WithContext(ctx, h.instrumentOpts)
   182  	)
   183  	currPlacement, err := h.placementGetHandler.Get(
   184  		h.serviceNameAndDefaults(), nil)
   185  	if err != nil {
   186  		logger.Error("unable to get placement", zap.Error(err))
   187  		xhttp.WriteError(w, err)
   188  		return
   189  	}
   190  
   191  	parsedReq, namespaceRequests, placementRequest, rErr := h.parseAndValidateRequest(r, currPlacement)
   192  	if rErr != nil {
   193  		logger.Error("unable to parse request", zap.Error(rErr))
   194  		xhttp.WriteError(w, rErr)
   195  		return
   196  	}
   197  
   198  	currPlacement, err = h.maybeInitPlacement(currPlacement, parsedReq, placementRequest, r)
   199  	if err != nil {
   200  		logger.Error("unable to initialize placement", zap.Error(err))
   201  		xhttp.WriteError(w, err)
   202  		return
   203  	}
   204  
   205  	opts := handleroptions.NewServiceOptions(h.serviceNameAndDefaults(),
   206  		r.Header, nil)
   207  	nsRegistry, err := h.namespaceGetHandler.Get(opts)
   208  	if err != nil {
   209  		logger.Error("unable to retrieve existing namespaces", zap.Error(err))
   210  		xhttp.WriteError(w, err)
   211  		return
   212  	}
   213  
   214  	// TODO(rartoul): Add test for NS exists.
   215  	for _, namespaceRequest := range namespaceRequests {
   216  		if _, nsExists := nsRegistry.Namespaces[namespaceRequest.Name]; nsExists {
   217  			err := xerrors.NewInvalidParamsError(fmt.Errorf(
   218  				"unable to create namespace: %s because it already exists",
   219  				namespaceRequest.Name))
   220  			logger.Error("unable to create namespace", zap.Error(err))
   221  			xhttp.WriteError(w, err)
   222  			return
   223  		}
   224  	}
   225  
   226  	for _, namespaceRequest := range namespaceRequests {
   227  		nsRegistry, err = h.namespaceAddHandler.Add(namespaceRequest, opts)
   228  		if err != nil {
   229  			logger.Error("unable to add namespace", zap.Error(err))
   230  			xhttp.WriteError(w, err)
   231  			return
   232  		}
   233  	}
   234  
   235  	placementProto, err := currPlacement.Proto()
   236  	if err != nil {
   237  		logger.Error("unable to get placement protobuf", zap.Error(err))
   238  		xhttp.WriteError(w, err)
   239  		return
   240  	}
   241  
   242  	resp := &admin.DatabaseCreateResponse{
   243  		Namespace: &admin.NamespaceGetResponse{
   244  			Registry: &nsRegistry,
   245  		},
   246  		Placement: &admin.PlacementGetResponse{
   247  			Placement: placementProto,
   248  		},
   249  	}
   250  
   251  	xhttp.WriteProtoMsgJSONResponse(w, resp, logger)
   252  }
   253  
   254  func (h *createHandler) maybeInitPlacement(
   255  	currPlacement clusterplacement.Placement,
   256  	parsedReq *admin.DatabaseCreateRequest,
   257  	placementRequest *admin.PlacementInitRequest,
   258  	r *http.Request,
   259  ) (clusterplacement.Placement, error) {
   260  	if currPlacement == nil {
   261  		// If we're here then there is no existing placement, so just create it. This is safe because in
   262  		// the case where a placement did not already exist, the parse function above validated that we
   263  		// have all the required information to create a placement.
   264  		newPlacement, err := h.placementInitHandler.Init(h.serviceNameAndDefaults(),
   265  			r, placementRequest)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  
   270  		return newPlacement, nil
   271  	}
   272  
   273  	// NB(rartoul): Pardon the branchiness, making sure every permutation is "reasoned" through for
   274  	// the scenario where the placement already exists.
   275  	switch dbType(parsedReq.Type) {
   276  	case dbTypeCluster:
   277  		if placementRequest != nil {
   278  			// If the caller has specified a desired clustered placement and a placement already exists,
   279  			// throw an error because the create database API should not be used for modifying clustered
   280  			// placements. Instead, they should use the placement APIs.
   281  			return nil, errClusteredPlacementAlreadyExists
   282  		}
   283  
   284  		if placementIsLocal(currPlacement) {
   285  			// If the caller has specified that they desire a clustered placement (without specifying hosts)
   286  			// and a local placement already exists then throw an error because we can't ignore their request
   287  			// and we also can't convert a local placement to a clustered one.
   288  			return nil, errCantReplaceLocalPlacementWithClustered
   289  		}
   290  
   291  		// This is fine because we'll just assume they want to keep the same clustered placement
   292  		// that they already have because they didn't specify any hosts.
   293  		return currPlacement, nil
   294  	case dbTypeLocal:
   295  		if !placementIsLocal(currPlacement) {
   296  			// If the caller has specified that they desire a local placement and a clustered placement
   297  			// already exists then throw an error because we can't ignore their request and we also can't
   298  			// convert a clustered placement to a local one.
   299  			return nil, errCantReplaceLocalPlacementWithClustered
   300  		}
   301  
   302  		// This is fine because we'll just assume they want to keep the same local placement
   303  		// that they already have.
   304  		return currPlacement, nil
   305  	case "":
   306  		// This is fine because we'll just assume they want to keep the same placement that they already
   307  		// have.
   308  		return currPlacement, nil
   309  	default:
   310  		// Invalid dbType.
   311  		return nil, xerrors.NewInvalidParamsError(fmt.Errorf("unknown database type: %s", parsedReq.Type))
   312  	}
   313  }
   314  
   315  func (h *createHandler) parseAndValidateRequest(
   316  	r *http.Request,
   317  	existingPlacement clusterplacement.Placement,
   318  ) (*admin.DatabaseCreateRequest, []*admin.NamespaceAddRequest, *admin.PlacementInitRequest, error) {
   319  	requirePlacement := existingPlacement == nil
   320  
   321  	defer r.Body.Close() //nolint:errcheck
   322  	rBody, err := xhttp.DurationToNanosBytes(r.Body)
   323  	if err != nil {
   324  		wrapped := fmt.Errorf("error converting duration to nano bytes: %s", err.Error())
   325  		return nil, nil, nil, xerrors.NewInvalidParamsError(wrapped)
   326  	}
   327  
   328  	dbCreateReq := new(admin.DatabaseCreateRequest)
   329  	if err := jsonpb.Unmarshal(bytes.NewReader(rBody), dbCreateReq); err != nil {
   330  		return nil, nil, nil, xerrors.NewInvalidParamsError(err)
   331  	}
   332  
   333  	if dbCreateReq.NamespaceName == "" {
   334  		err := fmt.Errorf("%s: namespaceName", errMissingRequiredField)
   335  		return nil, nil, nil, xerrors.NewInvalidParamsError(err)
   336  	}
   337  
   338  	requestedDBType := dbType(dbCreateReq.Type)
   339  	if requirePlacement &&
   340  		requestedDBType == dbTypeCluster &&
   341  		len(dbCreateReq.Hosts) == 0 {
   342  		return nil, nil, nil, xerrors.NewInvalidParamsError(errMissingRequiredField)
   343  	}
   344  
   345  	namespaceAddRequests, err := defaultedNamespaceAddRequests(dbCreateReq, existingPlacement)
   346  	if err != nil {
   347  		return nil, nil, nil, xerrors.NewInvalidParamsError(err)
   348  	}
   349  
   350  	var placementInitRequest *admin.PlacementInitRequest
   351  	if (requestedDBType == dbTypeCluster && len(dbCreateReq.Hosts) > 0) ||
   352  		requestedDBType == dbTypeLocal {
   353  		placementInitRequest, err = defaultedPlacementInitRequest(dbCreateReq, h.embeddedDBCfg)
   354  		if err != nil {
   355  			return nil, nil, nil, xerrors.NewInvalidParamsError(err)
   356  		}
   357  	}
   358  
   359  	return dbCreateReq, namespaceAddRequests, placementInitRequest, nil
   360  }
   361  
   362  func defaultedNamespaceAddRequests(
   363  	r *admin.DatabaseCreateRequest,
   364  	existingPlacement clusterplacement.Placement,
   365  ) ([]*admin.NamespaceAddRequest, error) {
   366  	var (
   367  		dbType = dbType(r.Type)
   368  	)
   369  	if dbType == "" && existingPlacement != nil {
   370  		// If they didn't provide a database type, infer it from the
   371  		// existing placement.
   372  		if placementIsLocal(existingPlacement) {
   373  			dbType = dbTypeLocal
   374  		} else {
   375  			dbType = dbTypeCluster
   376  		}
   377  	}
   378  
   379  	nsAddRequests := make([]*admin.NamespaceAddRequest, 0, 2)
   380  	switch dbType {
   381  	case dbTypeLocal, dbTypeCluster:
   382  		unaggregatedNs, err := defaultedUnaggregatedNamespaceAddRequest(r)
   383  		if err != nil {
   384  			return nil, err
   385  		}
   386  		nsAddRequests = append(nsAddRequests, unaggregatedNs)
   387  
   388  		aggregatedNs, err := defaultedAggregatedNamespaceAddRequest(r)
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  		if aggregatedNs != nil {
   393  			nsAddRequests = append(nsAddRequests, aggregatedNs)
   394  		}
   395  	default:
   396  		return nil, errInvalidDBType
   397  	}
   398  
   399  	return nsAddRequests, nil
   400  }
   401  
   402  func defaultedUnaggregatedNamespaceAddRequest(
   403  	r *admin.DatabaseCreateRequest,
   404  ) (*admin.NamespaceAddRequest, error) {
   405  	opts := dbnamespace.NewOptions().
   406  		SetRepairEnabled(false)
   407  	retentionOpts := opts.RetentionOptions()
   408  
   409  	if r.RetentionTime == "" {
   410  		retentionOpts = retentionOpts.SetRetentionPeriod(defaultLocalRetentionPeriod)
   411  	} else {
   412  		value, err := time.ParseDuration(r.RetentionTime)
   413  		if err != nil {
   414  			return nil, fmt.Errorf("invalid retention time: %v", err)
   415  		}
   416  		retentionOpts = retentionOpts.SetRetentionPeriod(value)
   417  	}
   418  
   419  	retentionPeriod := retentionOpts.RetentionPeriod()
   420  
   421  	var blockSize time.Duration
   422  	switch {
   423  	case r.BlockSize != nil && r.BlockSize.Time != "":
   424  		value, err := time.ParseDuration(r.BlockSize.Time)
   425  		if err != nil {
   426  			return nil, fmt.Errorf("invalid block size time: %v", err)
   427  		}
   428  		blockSize = value
   429  
   430  	case r.BlockSize != nil && r.BlockSize.ExpectedSeriesDatapointsPerHour > 0:
   431  		value := r.BlockSize.ExpectedSeriesDatapointsPerHour
   432  		blockSize = time.Duration(blockSizeFromExpectedSeriesScalar / value)
   433  		// Snap to the nearest 5mins
   434  		blockSizeCeil := blockSize.Truncate(5*time.Minute) + 5*time.Minute
   435  		blockSizeFloor := blockSize.Truncate(5 * time.Minute)
   436  		if blockSizeFloor%time.Hour == 0 ||
   437  			blockSizeFloor%30*time.Minute == 0 ||
   438  			blockSizeFloor%15*time.Minute == 0 ||
   439  			blockSizeFloor%10*time.Minute == 0 {
   440  			// Try snap to hour or 30min or 15min or 10min boundary if possible
   441  			blockSize = blockSizeFloor
   442  		} else {
   443  			blockSize = blockSizeCeil
   444  		}
   445  
   446  		if blockSize < minRecommendCalculateBlockSize {
   447  			blockSize = minRecommendCalculateBlockSize
   448  		} else if blockSize > maxRecommendCalculateBlockSize {
   449  			blockSize = maxRecommendCalculateBlockSize
   450  		}
   451  
   452  	default:
   453  		blockSize = getRecommendedBlockSize(retentionPeriod)
   454  	}
   455  
   456  	retentionOpts = retentionOpts.SetBlockSize(blockSize)
   457  
   458  	indexOpts := opts.IndexOptions().
   459  		SetEnabled(true).
   460  		SetBlockSize(blockSize)
   461  
   462  	opts = opts.SetRetentionOptions(retentionOpts).
   463  		SetIndexOptions(indexOpts)
   464  
   465  	// Resolution does not apply to unaggregated namespaces so set to 0.
   466  	opts = opts.SetAggregationOptions(dbnamespace.NewAggregationOptions().
   467  		SetAggregations([]dbnamespace.Aggregation{
   468  			dbnamespace.NewUnaggregatedAggregation(),
   469  		}))
   470  
   471  	optsProto, err := dbnamespace.OptionsToProto(opts)
   472  	if err != nil {
   473  		return nil, err
   474  	}
   475  
   476  	return &admin.NamespaceAddRequest{
   477  		Name:    r.NamespaceName,
   478  		Options: optsProto,
   479  	}, nil
   480  }
   481  
   482  func defaultedAggregatedNamespaceAddRequest(
   483  	r *admin.DatabaseCreateRequest,
   484  ) (*admin.NamespaceAddRequest, error) {
   485  	agg := r.AggregatedNamespace
   486  	if agg == nil {
   487  		return nil, nil
   488  	}
   489  
   490  	if agg.Name == "" {
   491  		return nil, errors.New("name required when aggregatedNamespace is set")
   492  	}
   493  
   494  	if agg.Resolution == "" {
   495  		return nil, errors.New("resolution required when aggregatedNamespace is set")
   496  	}
   497  
   498  	if agg.RetentionTime == "" {
   499  		return nil, errors.New("retention_time required when aggregatedNamespace is set")
   500  	}
   501  
   502  	opts := dbnamespace.NewOptions().
   503  		SetRepairEnabled(false)
   504  
   505  	retentionOpts := opts.RetentionOptions()
   506  	retentionPeriod, err := time.ParseDuration(agg.RetentionTime)
   507  	if err != nil {
   508  		return nil, fmt.Errorf("invalid retention time: %v", err)
   509  	}
   510  	retentionOpts = retentionOpts.SetRetentionPeriod(retentionPeriod)
   511  
   512  	blockSize := getRecommendedBlockSize(retentionPeriod)
   513  
   514  	retentionOpts = retentionOpts.SetBlockSize(blockSize)
   515  
   516  	indexOpts := opts.IndexOptions().
   517  		SetEnabled(true).
   518  		SetBlockSize(blockSize)
   519  
   520  	opts = opts.SetRetentionOptions(retentionOpts).
   521  		SetIndexOptions(indexOpts)
   522  
   523  	resolution, err := time.ParseDuration(agg.Resolution)
   524  	if err != nil {
   525  		return nil, fmt.Errorf("invalid resolution: %v", err)
   526  	}
   527  
   528  	attrs, err := dbnamespace.NewAggregatedAttributes(resolution, dbnamespace.NewDownsampleOptions(true))
   529  	if err != nil {
   530  		return nil, err
   531  	}
   532  
   533  	opts = opts.SetAggregationOptions(dbnamespace.NewAggregationOptions().
   534  		SetAggregations([]dbnamespace.Aggregation{
   535  			dbnamespace.NewAggregatedAggregation(attrs),
   536  		}))
   537  
   538  	optsProto, err := dbnamespace.OptionsToProto(opts)
   539  	if err != nil {
   540  		return nil, err
   541  	}
   542  
   543  	return &admin.NamespaceAddRequest{
   544  		Name:    agg.Name,
   545  		Options: optsProto,
   546  	}, nil
   547  }
   548  
   549  func getRecommendedBlockSize(retentionPeriod time.Duration) time.Duration {
   550  	// Use the maximum block size if we don't find a way to
   551  	// recommended one based on request parameters
   552  	max := recommendedBlockSizesByRetentionAsc[len(recommendedBlockSizesByRetentionAsc)-1]
   553  	blockSize := max.blockSize
   554  	for _, elem := range recommendedBlockSizesByRetentionAsc {
   555  		if retentionPeriod <= elem.forRetentionLessThanOrEqual {
   556  			blockSize = elem.blockSize
   557  			break
   558  		}
   559  	}
   560  	return blockSize
   561  }
   562  
   563  func defaultedPlacementInitRequest(
   564  	r *admin.DatabaseCreateRequest,
   565  	embeddedDBCfg *dbconfig.DBConfiguration,
   566  ) (*admin.PlacementInitRequest, error) {
   567  	var (
   568  		numShards         int32
   569  		replicationFactor int32
   570  		instances         []*placementpb.Instance
   571  	)
   572  	switch dbType(r.Type) {
   573  	case dbTypeLocal:
   574  		if embeddedDBCfg == nil {
   575  			return nil, errMissingEmbeddedDBConfig
   576  		}
   577  
   578  		addr := embeddedDBCfg.ListenAddressOrDefault()
   579  		port, err := portFromEmbeddedDBConfigListenAddress(addr)
   580  		if err != nil {
   581  			return nil, err
   582  		}
   583  
   584  		numShards = shardMultiplier
   585  		replicationFactor = 1
   586  		instances = []*placementpb.Instance{
   587  			{
   588  				Id:             DefaultLocalHostID,
   589  				IsolationGroup: DefaultLocalIsolationGroup,
   590  				Zone:           DefaultLocalZone,
   591  				Weight:         1,
   592  				Endpoint:       fmt.Sprintf("127.0.0.1:%d", port),
   593  				Hostname:       "localhost",
   594  				Port:           uint32(port),
   595  			},
   596  		}
   597  	case dbTypeCluster:
   598  
   599  		numHosts := len(r.Hosts)
   600  		numShards = int32(math.Min(math.MaxInt32, powerOfTwoAtLeast(float64(numHosts*shardMultiplier))))
   601  		replicationFactor = r.ReplicationFactor
   602  		if replicationFactor == 0 {
   603  			replicationFactor = 3
   604  		}
   605  
   606  		instances = make([]*placementpb.Instance, 0, numHosts)
   607  
   608  		for _, host := range r.Hosts {
   609  			id := strings.TrimSpace(host.Id)
   610  			if id == "" {
   611  				return nil, errMissingHostID
   612  			}
   613  
   614  			isolationGroup := strings.TrimSpace(host.IsolationGroup)
   615  			if isolationGroup == "" {
   616  				isolationGroup = defaultIsolationGroup
   617  			}
   618  
   619  			zone := strings.TrimSpace(host.Zone)
   620  			if zone == "" {
   621  				zone = defaultZone
   622  			}
   623  
   624  			weight := host.Weight
   625  			if weight == 0 {
   626  				weight = 1
   627  			}
   628  
   629  			instances = append(instances, &placementpb.Instance{
   630  				Id:             id,
   631  				IsolationGroup: isolationGroup,
   632  				Zone:           zone,
   633  				Weight:         weight,
   634  				Endpoint:       fmt.Sprintf("%s:%d", host.Address, host.Port),
   635  				Hostname:       host.Address,
   636  				Port:           host.Port,
   637  			})
   638  		}
   639  	default:
   640  		return nil, errInvalidDBType
   641  	}
   642  
   643  	numShardsInput := r.GetNumShards()
   644  	if numShardsInput > 0 {
   645  		numShards = numShardsInput
   646  	}
   647  
   648  	return &admin.PlacementInitRequest{
   649  		NumShards:         numShards,
   650  		ReplicationFactor: replicationFactor,
   651  		Instances:         instances,
   652  	}, nil
   653  }
   654  
   655  func placementIsLocal(p clusterplacement.Placement) bool {
   656  	existingInstances := p.Instances()
   657  	return len(existingInstances) == 1 &&
   658  		existingInstances[0].ID() == DefaultLocalHostID &&
   659  		existingInstances[0].IsolationGroup() == DefaultLocalIsolationGroup &&
   660  		existingInstances[0].Zone() == DefaultLocalZone
   661  }
   662  
   663  func portFromEmbeddedDBConfigListenAddress(address string) (int, error) {
   664  	colonIdx := strings.LastIndex(address, ":")
   665  	if colonIdx == -1 || colonIdx == len(address)-1 {
   666  		return 0, errMissingEmbeddedDBPort
   667  	}
   668  
   669  	return strconv.Atoi(address[colonIdx+1:])
   670  }
   671  
   672  func powerOfTwoAtLeast(num float64) float64 {
   673  	return math.Pow(2, math.Ceil(math.Log2(num)))
   674  }