Skip to content

Commit

Permalink
feat: Autoscaler (#1077)
Browse files Browse the repository at this point in the history
* work in progress - broken

* Add autoscaling options to interface

* Work in progress on function for setting metadata

* Fixed some tests that were broken due to the chang

* Cluster.ts in test file

* Update cluster creation to accept new parameters

* Fix for failing test

* Enhance system tests for cluster

* Add another test

* condense tests

* Added more tests

* Test improvements

* Change update mask to get manual scaling right

* Added validation for cluster creation configs

* Refactor cluster object in tests

* Another cluster id refactor

* Remove TODO that no longer applies

* Code reorganization for test cases

* Fix the test so that it breaks for good reason

* update mask push fix

* Add nodes in all calls to update and create cluste

* Add license headers

* correct copyright year

* Add singlequote

* Add a validation

* Update test descriptions

* refactor to use one check metadata function

* Fix tests as specifying nodes is required anyway

* should added to test case descriptions

* PR updates

* validation error

* Remove call to get metadata

* PR updates
  • Loading branch information
danieljbruce committed May 31, 2022
1 parent 033bfc8 commit e5f6fdb
Show file tree
Hide file tree
Showing 9 changed files with 667 additions and 50 deletions.
31 changes: 16 additions & 15 deletions src/cluster.ts
Expand Up @@ -22,6 +22,7 @@ const pumpify = require('pumpify');
import {google} from '../protos/protos';
import {Bigtable} from '.';
import {Instance} from './instance';
import {ClusterUtils} from './utils/cluster';

import {
Backup,
Expand Down Expand Up @@ -75,7 +76,10 @@ export type GetClustersCallback = (
apiResponse?: google.bigtable.admin.v2.IListClustersResponse
) => void;
export interface SetClusterMetadataOptions {
nodes: number;
nodes?: number;
minServeNodes?: number;
maxServeNodes?: number;
cpuUtilizationPercent?: number;
}
export type SetClusterMetadataCallback = GenericOperationCallback<
Operation | null | undefined
Expand All @@ -84,8 +88,11 @@ export interface BasicClusterConfig {
encryption?: google.bigtable.admin.v2.Cluster.IEncryptionConfig;
key?: string;
location: string;
nodes: number;
nodes?: number;
storage?: string;
minServeNodes?: number;
maxServeNodes?: number;
cpuUtilizationPercent?: number;
}

export interface CreateBackupConfig extends ModifiableBackupFields {
Expand Down Expand Up @@ -690,29 +697,23 @@ Please use the format 'my-cluster' or '${instance.name}/clusters/my-cluster'.`);
gaxOptionsOrCallback?: CallOptions | SetClusterMetadataCallback,
cb?: SetClusterMetadataCallback
): void | Promise<SetClusterMetadataResponse> {
ClusterUtils.validateClusterMetadata(metadata);
const callback =
typeof gaxOptionsOrCallback === 'function' ? gaxOptionsOrCallback : cb!;
const gaxOptions =
typeof gaxOptionsOrCallback === 'object'
? gaxOptionsOrCallback
: ({} as CallOptions);

const reqOpts: ICluster = Object.assign(
{},
{
name: this.name,
serveNodes: metadata.nodes,
},
metadata
const reqOpts = ClusterUtils.getRequestFromMetadata(
metadata,
this?.metadata?.location,
this.name
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (reqOpts as any).nodes;

this.bigtable.request<Operation>(
{
client: 'BigtableInstanceAdminClient',
method: 'updateCluster',
reqOpts,
method: 'partialUpdateCluster',
reqOpts: reqOpts,
gaxOpts: gaxOptions,
},
(err, resp) => {
Expand Down
14 changes: 9 additions & 5 deletions src/index.ts
Expand Up @@ -34,6 +34,7 @@ import {ServiceError} from 'google-gax';
import * as v2 from './v2';
import {PassThrough, Duplex} from 'stream';
import grpcGcpModule = require('grpc-gcp');
import {ClusterUtils} from './utils/cluster';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const streamEvents = require('stream-events');
Expand Down Expand Up @@ -610,12 +611,15 @@ export class Bigtable {
'A cluster was provided with both `encryption` and `key` defined.'
);
}

clusters[cluster.id!] = {
location: Cluster.getLocation_(this.projectId, cluster.location!),
serveNodes: cluster.nodes,
ClusterUtils.validateClusterMetadata(cluster);
clusters[cluster.id!] = ClusterUtils.getClusterBaseConfig(
cluster,
Cluster.getLocation_(this.projectId, cluster.location!),
undefined
);
Object.assign(clusters[cluster.id!], {
defaultStorageType: Cluster.getStorageType_(cluster.storage!),
};
});

if (cluster.key) {
clusters[cluster.id!].encryptionConfig = {
Expand Down
22 changes: 9 additions & 13 deletions src/instance.ts
Expand Up @@ -67,6 +67,7 @@ import {ServiceError} from 'google-gax';
import {Bigtable} from '.';
import {google} from '../protos/protos';
import {Backup, RestoreTableCallback, RestoreTableResponse} from './backup';
import {ClusterUtils} from './utils/cluster';

export interface ClusterInfo extends BasicClusterConfig {
id: string;
Expand Down Expand Up @@ -391,9 +392,15 @@ Please use the format 'my-instance' or '${bigtable.projectName}/instances/my-ins
parent: this.name,
clusterId: id,
} as google.bigtable.admin.v2.CreateClusterRequest;

ClusterUtils.validateClusterMetadata(options);
if (!is.empty(options)) {
reqOpts.cluster = {};
reqOpts.cluster = ClusterUtils.getClusterBaseConfig(
options,
options.location
? Cluster.getLocation_(this.bigtable.projectId, options.location)
: undefined,
undefined
);
}

if (
Expand All @@ -415,17 +422,6 @@ Please use the format 'my-instance' or '${bigtable.projectName}/instances/my-ins
reqOpts.cluster!.encryptionConfig = options.encryption;
}

if (options.location) {
reqOpts.cluster!.location = Cluster.getLocation_(
this.bigtable.projectId,
options.location
);
}

if (options.nodes) {
reqOpts.cluster!.serveNodes = options.nodes;
}

if (options.storage) {
const storageType = Cluster.getStorageType_(options.storage);
reqOpts.cluster!.defaultStorageType = storageType;
Expand Down
153 changes: 153 additions & 0 deletions src/utils/cluster.ts
@@ -0,0 +1,153 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as protos from '../../protos/protos';
import {
BasicClusterConfig,
ICluster,
SetClusterMetadataOptions,
} from '../cluster';
import {google} from '../../protos/protos';

export class ClusterUtils {
static noConfigError =
'Must specify either serve_nodes or all of the autoscaling configurations (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent).';
static allConfigError =
'Cannot specify both serve_nodes and autoscaling configurations (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent).';
static incompleteConfigError =
'All of autoscaling configurations must be specified at the same time (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent).';

static validateClusterMetadata(
metadata: SetClusterMetadataOptions | BasicClusterConfig
): void {
if (metadata.nodes) {
if (
metadata.minServeNodes ||
metadata.maxServeNodes ||
metadata.cpuUtilizationPercent
) {
throw new Error(this.allConfigError);
}
} else {
if (
metadata.minServeNodes ||
metadata.maxServeNodes ||
metadata.cpuUtilizationPercent
) {
if (
!(
metadata.minServeNodes &&
metadata.maxServeNodes &&
metadata.cpuUtilizationPercent
)
) {
throw new Error(this.incompleteConfigError);
}
} else {
throw new Error(this.noConfigError);
}
}
}
static getUpdateMask(metadata: SetClusterMetadataOptions): string[] {
const updateMask: string[] = [];
if (metadata.nodes) {
updateMask.push('serve_nodes');
if (
!(
metadata.minServeNodes ||
metadata.maxServeNodes ||
metadata.cpuUtilizationPercent
)
) {
updateMask.push('cluster_config.cluster_autoscaling_config');
}
}
if (metadata.minServeNodes) {
updateMask.push(
'cluster_config.cluster_autoscaling_config.autoscaling_limits.min_serve_nodes'
);
}
if (metadata.maxServeNodes) {
updateMask.push(
'cluster_config.cluster_autoscaling_config.autoscaling_limits.max_serve_nodes'
);
}
if (metadata.cpuUtilizationPercent) {
updateMask.push(
'cluster_config.cluster_autoscaling_config.autoscaling_targets.cpu_utilization_percent'
);
}
return updateMask;
}

static getClusterBaseConfig(
metadata: SetClusterMetadataOptions | BasicClusterConfig,
location: string | undefined | null,
name: string | undefined
): google.bigtable.admin.v2.ICluster {
let clusterConfig;
if (
metadata.cpuUtilizationPercent ||
metadata.minServeNodes ||
metadata.maxServeNodes
) {
clusterConfig = {
clusterAutoscalingConfig: {
autoscalingTargets: {
cpuUtilizationPercent: metadata.cpuUtilizationPercent,
},
autoscalingLimits: {
minServeNodes: metadata.minServeNodes,
maxServeNodes: metadata.maxServeNodes,
},
},
};
}
return Object.assign(
{},
name ? {name} : null,
location ? {location} : null,
clusterConfig ? {clusterConfig} : null,
metadata.nodes ? {serveNodes: metadata.nodes} : null
);
}

static getClusterFromMetadata(
metadata: SetClusterMetadataOptions,
location: string | undefined | null,
name: string
): google.bigtable.admin.v2.ICluster {
const cluster: ICluster | SetClusterMetadataOptions = Object.assign(
{},
this.getClusterBaseConfig(metadata, location, name),
metadata
);
delete (cluster as SetClusterMetadataOptions).nodes;
delete (cluster as SetClusterMetadataOptions).minServeNodes;
delete (cluster as SetClusterMetadataOptions).maxServeNodes;
delete (cluster as SetClusterMetadataOptions).cpuUtilizationPercent;
return cluster as ICluster;
}

static getRequestFromMetadata(
metadata: SetClusterMetadataOptions,
location: string | undefined | null,
name: string
): protos.google.bigtable.admin.v2.IPartialUpdateClusterRequest {
return {
cluster: this.getClusterFromMetadata(metadata, location, name),
updateMask: {paths: this.getUpdateMask(metadata)},
};
}
}

0 comments on commit e5f6fdb

Please sign in to comment.