Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Managed Autoscalar feature #1038

Closed
wants to merge 13 commits into from
Closed
6 changes: 6 additions & 0 deletions google/cloud/spanner_v1/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def instance(
node_count=None,
labels=None,
processing_units=None,
autoscaling_config=None,
):
"""Factory to create a instance associated with this client.

Expand Down Expand Up @@ -340,6 +341,10 @@ def instance(
:type labels: dict (str -> str) or None
:param labels: (Optional) User-assigned labels for this instance.

:type autoscaling_config:
:class:`~google.cloud.spanner_admin_instance_v1.types.AutoscalingConfig`
:param autoscaling_config: (Optional) The autoscaling configuration for this instance.

:rtype: :class:`~google.cloud.spanner_v1.instance.Instance`
:returns: an instance owned by this client.
"""
Expand All @@ -352,6 +357,7 @@ def instance(
self._emulator_host,
labels,
processing_units,
autoscaling_config,
)

def list_instances(self, filter_="", page_size=None):
Expand Down
44 changes: 38 additions & 6 deletions google/cloud/spanner_v1/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def __init__(
emulator_host=None,
labels=None,
processing_units=None,
autoscaling_config=None,
):
self.instance_id = instance_id
self._client = client
Expand All @@ -131,20 +132,28 @@ def __init__(
raise InvalidArgument(
"Only one of node count and processing units can be set."
)
if node_count is None and processing_units is None:
if (
node_count is None
and processing_units is None
and autoscaling_config is None
):
self._node_count = DEFAULT_NODE_COUNT
self._processing_units = DEFAULT_NODE_COUNT * PROCESSING_UNITS_PER_NODE
elif node_count is not None:
self._node_count = node_count
self._processing_units = node_count * PROCESSING_UNITS_PER_NODE
else:
elif processing_units is not None:
self._processing_units = processing_units
self._node_count = processing_units // PROCESSING_UNITS_PER_NODE
else:
self._node_count = None
self._processing_units = None
self.display_name = display_name or instance_id
self.emulator_host = emulator_host
if labels is None:
labels = {}
self.labels = labels
self._autoscaling_config = autoscaling_config
surbhigarg92 marked this conversation as resolved.
Show resolved Hide resolved

def _update_from_pb(self, instance_pb):
"""Refresh self from the server-provided protobuf.
Expand All @@ -158,6 +167,7 @@ def _update_from_pb(self, instance_pb):
self._node_count = instance_pb.node_count
self._processing_units = instance_pb.processing_units
self.labels = instance_pb.labels
self._autoscaling_config = instance_pb.autoscaling_config

@classmethod
def from_pb(cls, instance_pb, client):
Expand Down Expand Up @@ -250,6 +260,14 @@ def node_count(self, value):
self._node_count = value
self._processing_units = value * PROCESSING_UNITS_PER_NODE

@property
def autoscaling_config(self):
return self._autoscaling_config

@autoscaling_config.setter
def autoscaling_config(self, value):
self._autoscaling_config = value

def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
Expand Down Expand Up @@ -281,6 +299,7 @@ def copy(self):
node_count=self._node_count,
processing_units=self._processing_units,
display_name=self.display_name,
autoscaling_config=self._autoscaling_config,
)

def create(self):
Expand Down Expand Up @@ -313,6 +332,7 @@ def create(self):
display_name=self.display_name,
processing_units=self._processing_units,
labels=self.labels,
autoscaling_config=self._autoscaling_config,
)
metadata = _metadata_with_prefix(self.name)

Expand Down Expand Up @@ -359,7 +379,7 @@ def reload(self):

self._update_from_pb(instance_pb)

def update(self):
def update(self, fields=None):
"""Update this instance.

See
Expand All @@ -377,6 +397,9 @@ def update(self):

before calling :meth:`update`.

:type fields: Sequence[str]
:param fields: a list of fields to update. Ex: ["autoscaling_config"]

:rtype: :class:`google.api_core.operation.Operation`
:returns: an operation instance
:raises NotFound: if the instance does not exist
Expand All @@ -389,12 +412,21 @@ def update(self):
node_count=self._node_count,
processing_units=self._processing_units,
labels=self.labels,
autoscaling_config=self._autoscaling_config,
)
# default field paths to update
paths = [
"config",
"display_name",
"processing_units",
"labels",
"autoscaling_config",
surbhigarg92 marked this conversation as resolved.
Show resolved Hide resolved
]
if fields is not None:
paths = fields

# Always update only processing_units, not nodes
field_mask = FieldMask(
paths=["config", "display_name", "processing_units", "labels"]
)
field_mask = FieldMask(paths=paths)
metadata = _metadata_with_prefix(self.name)

future = api.update_instance(
Expand Down
46 changes: 46 additions & 0 deletions samples/samples/snippets.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,52 @@ def create_instance_with_processing_units(instance_id, processing_units):
# [END spanner_create_instance_with_processing_units]


# [START spanner_create_instance_with_autoscaling_config]
def create_instance_with_autoscaling_config(instance_id):
"""Creates an instance."""
spanner_client = spanner.Client()

config_name = "{}/instanceConfigs/regional-us-west1".format(
spanner_client.project_name
)

autoscaling_config = spanner_instance_admin.AutoscalingConfig(
harshachinta marked this conversation as resolved.
Show resolved Hide resolved
autoscaling_limits=spanner_instance_admin.AutoscalingConfig.AutoscalingLimits(
min_nodes=1,
max_nodes=2,
),
autoscaling_targets=spanner_instance_admin.AutoscalingConfig.AutoscalingTargets(
high_priority_cpu_utilization_percent=65,
storage_utilization_percent=95,
),
)

instance = spanner_client.instance(
instance_id,
configuration_name=config_name,
display_name="Autoscaling config instance.",
labels={
"cloud_spanner_samples": "true",
"sample_name": "snippets-create_instance_with_autoscaling_config",
"created": str(int(time.time())),
},
autoscaling_config=autoscaling_config,
)

operation = instance.create()

print("Waiting for operation to complete...")
operation.result(OPERATION_TIMEOUT_SECONDS)
print(
"Created instance {} with {} autoscaling config".format(
instance_id, instance.autoscaling_config
)
)


# [END spanner_create_instance_with_autoscaling_config]


# [START spanner_get_instance_config]
def get_instance_config(instance_config):
"""Gets the leader options for the instance configuration."""
Expand Down
12 changes: 12 additions & 0 deletions samples/samples/snippets_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,18 @@ def test_create_instance_with_processing_units(capsys, lci_instance_id):
retry_429(instance.delete)()


def test_create_instance_with_autoscaling_config(capsys, lci_instance_id):
retry_429(snippets.create_instance_with_autoscaling_config)(
lci_instance_id,
)
out, _ = capsys.readouterr()
assert lci_instance_id in out
assert "autoscaling config" in out
spanner_client = spanner.Client()
instance = spanner_client.instance(lci_instance_id)
retry_429(instance.delete)()


def test_update_database(capsys, instance_id, sample_database):
snippets.update_database(instance_id, sample_database.database_id)
out, _ = capsys.readouterr()
Expand Down
92 changes: 92 additions & 0 deletions tests/system/test_instance_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,95 @@ def test_update_instance(
# other test cases.
shared_instance.display_name = old_display_name
shared_instance.update()


def test_create_instance_with_autoscaling_config(
not_emulator,
if_create_instance,
spanner_client,
instance_config,
instances_to_delete,
instance_operation_timeout,
):
from google.cloud.spanner_admin_instance_v1 import (
AutoscalingConfig as AutoscalingConfigPB,
)

autoscaling_config = AutoscalingConfigPB(
autoscaling_limits=AutoscalingConfigPB.AutoscalingLimits(
min_nodes=1,
max_nodes=2,
),
autoscaling_targets=AutoscalingConfigPB.AutoscalingTargets(
high_priority_cpu_utilization_percent=65,
storage_utilization_percent=95,
),
)

alt_instance_id = _helpers.unique_id("wpn")
instance = spanner_client.instance(
instance_id=alt_instance_id,
configuration_name=instance_config.name,
autoscaling_config=autoscaling_config,
)
operation = instance.create()
# Make sure this instance gets deleted after the test case.
instances_to_delete.append(instance)

# We want to make sure the operation completes.
operation.result(instance_operation_timeout) # raises on failure / timeout.

# Create a new instance instance and make sure it is the same.
instance_alt = spanner_client.instance(alt_instance_id, instance_config.name)
instance_alt.reload()

assert instance == instance_alt
assert instance.display_name == instance_alt.display_name
assert instance.autoscaling_config == instance_alt.autoscaling_config


def test_update_instance_with_autoscaling_config(
not_emulator,
spanner_client,
shared_instance,
shared_instance_id,
instance_operation_timeout,
):
from google.cloud.spanner_admin_instance_v1 import (
AutoscalingConfig as AutoscalingConfigPB,
)

autoscaling_config = AutoscalingConfigPB(
autoscaling_limits=AutoscalingConfigPB.AutoscalingLimits(
min_nodes=1,
max_nodes=2,
),
autoscaling_targets=AutoscalingConfigPB.AutoscalingTargets(
high_priority_cpu_utilization_percent=65,
storage_utilization_percent=95,
),
)
assert shared_instance.autoscaling_config != autoscaling_config

old_node_count = shared_instance.node_count
shared_instance.autoscaling_config = autoscaling_config
# Update only the autoscaling_config field. This is to ensure that
# node_count or processing_unit field is not considered during update,
# which might otherwise throw an error.
operation = shared_instance.update(fields=["autoscaling_config"])

# We want to make sure the operation completes.
operation.result(instance_operation_timeout) # raises on failure / timeout.

# Create a new instance instance and reload it.
instance_alt = spanner_client.instance(shared_instance_id, None)
assert instance_alt.autoscaling_config != autoscaling_config

instance_alt.reload()
assert instance_alt.autoscaling_config == autoscaling_config

# Make sure to put the instance back the way it was for the
# other test cases.
shared_instance.node_count = old_node_count
shared_instance.autoscaling_config = None
shared_instance.update()
24 changes: 23 additions & 1 deletion tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,24 @@ def test_list_instances(self):
from google.cloud.spanner_admin_instance_v1 import Instance as InstancePB
from google.cloud.spanner_admin_instance_v1 import ListInstancesRequest
from google.cloud.spanner_admin_instance_v1 import ListInstancesResponse
from google.cloud.spanner_admin_instance_v1 import (
AutoscalingConfig as AutoscalingConfigPB,
)

api = InstanceAdminClient(credentials=mock.Mock())
credentials = _make_credentials()
client = self._make_one(project=self.PROJECT, credentials=credentials)
client._instance_admin_api = api
autoscaling_config = AutoscalingConfigPB(
autoscaling_limits=AutoscalingConfigPB.AutoscalingLimits(
min_nodes=1,
max_nodes=2,
),
autoscaling_targets=AutoscalingConfigPB.AutoscalingTargets(
high_priority_cpu_utilization_percent=65,
storage_utilization_percent=95,
),
)

instance_pbs = ListInstancesResponse(
instances=[
Expand All @@ -601,7 +614,11 @@ def test_list_instances(self):
display_name=self.DISPLAY_NAME,
node_count=self.NODE_COUNT,
processing_units=self.PROCESSING_UNITS,
)
),
InstancePB(
name=self.INSTANCE_NAME,
autoscaling_config=autoscaling_config,
),
]
)

Expand All @@ -620,6 +637,11 @@ def test_list_instances(self):
self.assertEqual(instance.node_count, self.NODE_COUNT)
self.assertEqual(instance.processing_units, self.PROCESSING_UNITS)

instance1 = instances[1]
self.assertIsInstance(instance1, InstancePB)
self.assertEqual(instance.name, self.INSTANCE_NAME)
self.assertEqual(instance1.autoscaling_config, autoscaling_config)

expected_metadata = (
("google-cloud-resource-prefix", client.project_name),
("x-goog-request-params", "parent={}".format(client.project_name)),
Expand Down
Loading