Skip to content

Commit 8f4e197

Browse files
authored
feat: Add support for autoscaling (#509)
* feat: Add support for autoscaling - Add the parameters min_serve_nodes, max_serve_nodes, and cpu_utilization_percent - Create disable_autoscaling function - Update documentation and tests - Add validation when scaling config was not set correctly.
1 parent a8a92ee commit 8f4e197

File tree

6 files changed

+877
-15
lines changed

6 files changed

+877
-15
lines changed

docs/snippets.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,25 @@ def test_bigtable_update_cluster():
401401
assert cluster.serve_nodes == 4
402402

403403

404+
def test_bigtable_cluster_disable_autoscaling():
405+
# [START bigtable_api_cluster_disable_autoscaling]
406+
from google.cloud.bigtable import Client
407+
408+
client = Client(admin=True)
409+
instance = client.instance(INSTANCE_ID)
410+
# Create a cluster with autoscaling enabled
411+
cluster = instance.cluster(
412+
CLUSTER_ID, min_serve_nodes=1, max_serve_nodes=2, cpu_utilization_percent=10
413+
)
414+
instance.create(clusters=[cluster])
415+
416+
# Disable autoscaling
417+
cluster.disable_autoscaling(serve_nodes=4)
418+
# [END bigtable_api_cluster_disable_autoscaling]
419+
420+
assert cluster.serve_nodes == 4
421+
422+
404423
def test_bigtable_create_table():
405424
# [START bigtable_api_create_table]
406425
from google.api_core import exceptions

google/cloud/bigtable/cluster.py

Lines changed: 165 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import re
1919
from google.cloud.bigtable_admin_v2.types import instance
2020
from google.api_core.exceptions import NotFound
21+
from google.protobuf import field_mask_pb2
2122

2223

2324
_CLUSTER_NAME_RE = re.compile(
@@ -36,6 +37,7 @@ class Cluster(object):
3637
* :meth:`create` itself
3738
* :meth:`update` itself
3839
* :meth:`delete` itself
40+
* :meth:`disable_autoscaling` itself
3941
4042
:type cluster_id: str
4143
:param cluster_id: The ID of the cluster.
@@ -52,7 +54,9 @@ class Cluster(object):
5254
https://cloud.google.com/bigtable/docs/locations
5355
5456
:type serve_nodes: int
55-
:param serve_nodes: (Optional) The number of nodes in the cluster.
57+
:param serve_nodes: (Optional) The number of nodes in the cluster for manual scaling. If any of the
58+
autoscaling configuration are specified, then the autoscaling
59+
configuration will take precedent.
5660
5761
:type default_storage_type: int
5862
:param default_storage_type: (Optional) The type of storage
@@ -85,6 +89,27 @@ class Cluster(object):
8589
:data:`google.cloud.bigtable.enums.Cluster.State.CREATING`.
8690
:data:`google.cloud.bigtable.enums.Cluster.State.RESIZING`.
8791
:data:`google.cloud.bigtable.enums.Cluster.State.DISABLED`.
92+
93+
:type min_serve_nodes: int
94+
:param min_serve_nodes: (Optional) The minimum number of nodes to be set in the cluster for autoscaling.
95+
Must be 1 or greater.
96+
If specified, this configuration takes precedence over
97+
``serve_nodes``.
98+
If specified, then
99+
``max_serve_nodes`` and ``cpu_utilization_percent`` must be
100+
specified too.
101+
102+
:type max_serve_nodes: int
103+
:param max_serve_nodes: (Optional) The maximum number of nodes to be set in the cluster for autoscaling.
104+
If specified, this configuration
105+
takes precedence over ``serve_nodes``. If specified, then
106+
``min_serve_nodes`` and ``cpu_utilization_percent`` must be
107+
specified too.
108+
109+
:param cpu_utilization_percent: (Optional) The CPU utilization target for the cluster's workload for autoscaling.
110+
If specified, this configuration takes precedence over ``serve_nodes``. If specified, then
111+
``min_serve_nodes`` and ``max_serve_nodes`` must be
112+
specified too.
88113
"""
89114

90115
def __init__(
@@ -96,6 +121,9 @@ def __init__(
96121
default_storage_type=None,
97122
kms_key_name=None,
98123
_state=None,
124+
min_serve_nodes=None,
125+
max_serve_nodes=None,
126+
cpu_utilization_percent=None,
99127
):
100128
self.cluster_id = cluster_id
101129
self._instance = instance
@@ -104,10 +132,13 @@ def __init__(
104132
self.default_storage_type = default_storage_type
105133
self._kms_key_name = kms_key_name
106134
self._state = _state
135+
self.min_serve_nodes = min_serve_nodes
136+
self.max_serve_nodes = max_serve_nodes
137+
self.cpu_utilization_percent = cpu_utilization_percent
107138

108139
@classmethod
109140
def from_pb(cls, cluster_pb, instance):
110-
"""Creates an cluster instance from a protobuf.
141+
"""Creates a cluster instance from a protobuf.
111142
112143
For example:
113144
@@ -159,6 +190,17 @@ def _update_from_pb(self, cluster_pb):
159190

160191
self.location_id = cluster_pb.location.split("/")[-1]
161192
self.serve_nodes = cluster_pb.serve_nodes
193+
194+
self.min_serve_nodes = (
195+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_limits.min_serve_nodes
196+
)
197+
self.max_serve_nodes = (
198+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_limits.max_serve_nodes
199+
)
200+
self.cpu_utilization_percent = (
201+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_targets.cpu_utilization_percent
202+
)
203+
162204
self.default_storage_type = cluster_pb.default_storage_type
163205
if cluster_pb.encryption_config:
164206
self._kms_key_name = cluster_pb.encryption_config.kms_key_name
@@ -211,6 +253,42 @@ def kms_key_name(self):
211253
"""str: Customer managed encryption key for the cluster."""
212254
return self._kms_key_name
213255

256+
def _validate_scaling_config(self):
257+
"""Validate auto/manual scaling configuration before creating or updating."""
258+
259+
if (
260+
not self.serve_nodes
261+
and not self.min_serve_nodes
262+
and not self.max_serve_nodes
263+
and not self.cpu_utilization_percent
264+
):
265+
raise ValueError(
266+
"Must specify either serve_nodes or all of the autoscaling configurations (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent)."
267+
)
268+
if self.serve_nodes and (
269+
self.max_serve_nodes or self.min_serve_nodes or self.cpu_utilization_percent
270+
):
271+
raise ValueError(
272+
"Cannot specify both serve_nodes and autoscaling configurations (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent)."
273+
)
274+
if (
275+
(
276+
self.min_serve_nodes
277+
and (not self.max_serve_nodes or not self.cpu_utilization_percent)
278+
)
279+
or (
280+
self.max_serve_nodes
281+
and (not self.min_serve_nodes or not self.cpu_utilization_percent)
282+
)
283+
or (
284+
self.cpu_utilization_percent
285+
and (not self.min_serve_nodes or not self.max_serve_nodes)
286+
)
287+
):
288+
raise ValueError(
289+
"All of autoscaling configurations must be specified at the same time (min_serve_nodes, max_serve_nodes, and cpu_utilization_percent)."
290+
)
291+
214292
def __eq__(self, other):
215293
if not isinstance(other, self.__class__):
216294
return NotImplemented
@@ -290,7 +368,15 @@ def create(self):
290368
:rtype: :class:`~google.api_core.operation.Operation`
291369
:returns: The long-running operation corresponding to the
292370
create operation.
371+
372+
:raises: :class:`ValueError <exceptions.ValueError>` if the both ``serve_nodes`` and autoscaling configurations
373+
are set at the same time or if none of the ``serve_nodes`` or autoscaling configurations are set
374+
or if the autoscaling configurations are only partially set.
375+
293376
"""
377+
378+
self._validate_scaling_config()
379+
294380
client = self._instance._client
295381
cluster_pb = self._to_pb()
296382

@@ -323,20 +409,73 @@ def update(self):
323409
324410
before calling :meth:`update`.
325411
412+
If autoscaling is already enabled, manual scaling will be silently ignored.
413+
To disable autoscaling and enable manual scaling, use the :meth:`disable_autoscaling` instead.
414+
326415
:rtype: :class:`Operation`
327416
:returns: The long-running operation corresponding to the
328417
update operation.
418+
329419
"""
420+
330421
client = self._instance._client
331-
# We are passing `None` for third argument location.
332-
# Location is set only at the time of creation of a cluster
333-
# and can not be changed after cluster has been created.
334-
return client.instance_admin_client.update_cluster(
335-
request={
336-
"serve_nodes": self.serve_nodes,
337-
"name": self.name,
338-
"location": None,
339-
}
422+
423+
update_mask_pb = field_mask_pb2.FieldMask()
424+
425+
if self.serve_nodes:
426+
update_mask_pb.paths.append("serve_nodes")
427+
428+
if self.min_serve_nodes:
429+
update_mask_pb.paths.append(
430+
"cluster_config.cluster_autoscaling_config.autoscaling_limits.min_serve_nodes"
431+
)
432+
if self.max_serve_nodes:
433+
update_mask_pb.paths.append(
434+
"cluster_config.cluster_autoscaling_config.autoscaling_limits.max_serve_nodes"
435+
)
436+
if self.cpu_utilization_percent:
437+
update_mask_pb.paths.append(
438+
"cluster_config.cluster_autoscaling_config.autoscaling_targets.cpu_utilization_percent"
439+
)
440+
441+
cluster_pb = self._to_pb()
442+
cluster_pb.name = self.name
443+
444+
return client.instance_admin_client.partial_update_cluster(
445+
request={"cluster": cluster_pb, "update_mask": update_mask_pb}
446+
)
447+
448+
def disable_autoscaling(self, serve_nodes):
449+
"""
450+
Disable autoscaling by specifying the number of nodes.
451+
452+
For example:
453+
454+
.. literalinclude:: snippets.py
455+
:start-after: [START bigtable_api_cluster_disable_autoscaling]
456+
:end-before: [END bigtable_api_cluster_disable_autoscaling]
457+
:dedent: 4
458+
459+
:type serve_nodes: int
460+
:param serve_nodes: The number of nodes in the cluster.
461+
"""
462+
463+
client = self._instance._client
464+
465+
update_mask_pb = field_mask_pb2.FieldMask()
466+
467+
self.serve_nodes = serve_nodes
468+
self.min_serve_nodes = 0
469+
self.max_serve_nodes = 0
470+
self.cpu_utilization_percent = 0
471+
472+
update_mask_pb.paths.append("serve_nodes")
473+
update_mask_pb.paths.append("cluster_config.cluster_autoscaling_config")
474+
cluster_pb = self._to_pb()
475+
cluster_pb.name = self.name
476+
477+
return client.instance_admin_client.partial_update_cluster(
478+
request={"cluster": cluster_pb, "update_mask": update_mask_pb}
340479
)
341480

342481
def delete(self):
@@ -375,6 +514,7 @@ def _to_pb(self):
375514
location = client.instance_admin_client.common_location_path(
376515
client.project, self.location_id
377516
)
517+
378518
cluster_pb = instance.Cluster(
379519
location=location,
380520
serve_nodes=self.serve_nodes,
@@ -384,4 +524,18 @@ def _to_pb(self):
384524
cluster_pb.encryption_config = instance.Cluster.EncryptionConfig(
385525
kms_key_name=self._kms_key_name,
386526
)
527+
528+
if self.min_serve_nodes:
529+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_limits.min_serve_nodes = (
530+
self.min_serve_nodes
531+
)
532+
if self.max_serve_nodes:
533+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_limits.max_serve_nodes = (
534+
self.max_serve_nodes
535+
)
536+
if self.cpu_utilization_percent:
537+
cluster_pb.cluster_config.cluster_autoscaling_config.autoscaling_targets.cpu_utilization_percent = (
538+
self.cpu_utilization_percent
539+
)
540+
387541
return cluster_pb

google/cloud/bigtable/instance.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,9 @@ def create(
228228
serve_nodes=None,
229229
default_storage_type=None,
230230
clusters=None,
231+
min_serve_nodes=None,
232+
max_serve_nodes=None,
233+
cpu_utilization_percent=None,
231234
):
232235
"""Create this instance.
233236
@@ -303,12 +306,18 @@ def create(
303306
location_id=location_id,
304307
serve_nodes=serve_nodes,
305308
default_storage_type=default_storage_type,
309+
min_serve_nodes=None,
310+
max_serve_nodes=None,
311+
cpu_utilization_percent=None,
306312
)
307313
]
308314
elif (
309315
location_id is not None
310316
or serve_nodes is not None
311317
or default_storage_type is not None
318+
or min_serve_nodes is not None
319+
or max_serve_nodes is not None
320+
or cpu_utilization_percent is not None
312321
):
313322
raise ValueError(
314323
"clusters and one of location_id, serve_nodes, \
@@ -546,6 +555,9 @@ def cluster(
546555
serve_nodes=None,
547556
default_storage_type=None,
548557
kms_key_name=None,
558+
min_serve_nodes=None,
559+
max_serve_nodes=None,
560+
cpu_utilization_percent=None,
549561
):
550562
"""Factory to create a cluster associated with this instance.
551563
@@ -605,6 +617,9 @@ def cluster(
605617
serve_nodes=serve_nodes,
606618
default_storage_type=default_storage_type,
607619
kms_key_name=kms_key_name,
620+
min_serve_nodes=min_serve_nodes,
621+
max_serve_nodes=max_serve_nodes,
622+
cpu_utilization_percent=cpu_utilization_percent,
608623
)
609624

610625
def list_clusters(self):

tests/system/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,24 @@ def admin_cluster(admin_instance, admin_cluster_id, location_id, serve_nodes):
107107
)
108108

109109

110+
@pytest.fixture(scope="session")
111+
def admin_cluster_with_autoscaling(
112+
admin_instance,
113+
admin_cluster_id,
114+
location_id,
115+
min_serve_nodes,
116+
max_serve_nodes,
117+
cpu_utilization_percent,
118+
):
119+
return admin_instance.cluster(
120+
admin_cluster_id,
121+
location_id=location_id,
122+
min_serve_nodes=min_serve_nodes,
123+
max_serve_nodes=max_serve_nodes,
124+
cpu_utilization_percent=cpu_utilization_percent,
125+
)
126+
127+
110128
@pytest.fixture(scope="session")
111129
def admin_instance_populated(admin_instance, admin_cluster, in_emulator):
112130
# Emulator does not support instance admin operations (create / delete).
@@ -170,3 +188,18 @@ def instances_to_delete():
170188

171189
for instance in instances_to_delete:
172190
_helpers.retry_429(instance.delete)()
191+
192+
193+
@pytest.fixture(scope="session")
194+
def min_serve_nodes(in_emulator):
195+
return 1
196+
197+
198+
@pytest.fixture(scope="session")
199+
def max_serve_nodes(in_emulator):
200+
return 8
201+
202+
203+
@pytest.fixture(scope="session")
204+
def cpu_utilization_percent(in_emulator):
205+
return 10

0 commit comments

Comments
 (0)