Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
from typing import List, Optional

from google.api_core.exceptions import InternalServerError
from google.cloud._helpers import _datetime_to_pb_timestamp

from google.cloud.aio._cross_sync import CrossSync
from google.cloud.spanner_v1._async._helpers import _retry, _retry_on_aborted_exception
from google.cloud.spanner_v1._helpers import (
AtomicCounter,
_check_rst_stream_error,
_make_list_value_pb,
_make_list_value_pbs,
_make_value_pb,
_merge_client_context,
_merge_request_options,
_merge_Transaction_Options,
Expand Down Expand Up @@ -165,6 +168,49 @@ def delete(self, table, keyset):
# TODO: Decide if we should add a span event per mutation:
# https://github.com/googleapis/python-spanner/issues/1269

def send(self, queue, key, payload=None, deliver_time=None):
"""Send a message to a Cloud Spanner queue.

:type queue: str
:param queue: Name of the queue to which the message will be sent.

:type key: list
:param key: The primary key of the message to be sent.

:type payload: object
:param payload: (Optional) The payload of the message.

:type deliver_time: :class:`datetime.datetime`
:param deliver_time: (Optional) The time at which Spanner will begin attempting to deliver the message.
"""
send_kwargs = {"queue": queue, "key": _make_list_value_pb(key)}
if payload is not None:
send_kwargs["payload"] = _make_value_pb(payload)
if deliver_time is not None:
send_kwargs["deliver_time"] = _datetime_to_pb_timestamp(deliver_time)

send = Mutation.Send(**send_kwargs)
self._mutations.append(Mutation(send=send))

def ack(self, queue, key, ignore_not_found=None):
"""Acknowledge a message in a Cloud Spanner queue.

:type queue: str
:param queue: Name of the queue where the message to be acked is stored.

:type key: list
:param key: The primary key of the message to be acked.

:type ignore_not_found: bool
:param ignore_not_found: (Optional) Whether to ignore if the message does not exist.
"""
ack_kwargs = {"queue": queue, "key": _make_list_value_pb(key)}
if ignore_not_found is not None:
ack_kwargs["ignore_not_found"] = ignore_not_found

ack = Mutation.Ack(**ack_kwargs)
self._mutations.append(Mutation(ack=ack))


class Batch(_BatchBase):
"""Accumulate mutations for transmission during :meth:`commit`."""
Expand Down
46 changes: 46 additions & 0 deletions packages/google-cloud-spanner/google/cloud/spanner_v1/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
from typing import List, Optional

from google.api_core.exceptions import InternalServerError
from google.cloud._helpers import _datetime_to_pb_timestamp

from google.cloud.spanner_v1._helpers import (
AtomicCounter,
_check_rst_stream_error,
_make_list_value_pb,
_make_list_value_pbs,
_make_value_pb,
_merge_client_context,
_merge_request_options,
_merge_Transaction_Options,
Expand Down Expand Up @@ -142,6 +145,49 @@ def delete(self, table, keyset):
delete = Mutation.Delete(table=table, key_set=keyset._to_pb())
self._mutations.append(Mutation(delete=delete))

def send(self, queue, key, payload=None, deliver_time=None):
"""Send a message to a Cloud Spanner queue.

:type queue: str
:param queue: Name of the queue to which the message will be sent.

:type key: list
:param key: The primary key of the message to be sent.

:type payload: object
:param payload: (Optional) The payload of the message.

:type deliver_time: :class:`datetime.datetime`
:param deliver_time: (Optional) The time at which Spanner will begin attempting to deliver the message.
"""
send_kwargs = {"queue": queue, "key": _make_list_value_pb(key)}
if payload is not None:
send_kwargs["payload"] = _make_value_pb(payload)
if deliver_time is not None:
send_kwargs["deliver_time"] = _datetime_to_pb_timestamp(deliver_time)

send = Mutation.Send(**send_kwargs)
self._mutations.append(Mutation(send=send))

def ack(self, queue, key, ignore_not_found=None):
"""Acknowledge a message in a Cloud Spanner queue.

:type queue: str
:param queue: Name of the queue where the message to be acked is stored.

:type key: list
:param key: The primary key of the message to be acked.

:type ignore_not_found: bool
:param ignore_not_found: (Optional) Whether to ignore if the message does not exist.
"""
ack_kwargs = {"queue": queue, "key": _make_list_value_pb(key)}
if ignore_not_found is not None:
ack_kwargs["ignore_not_found"] = ignore_not_found

ack = Mutation.Ack(**ack_kwargs)
self._mutations.append(Mutation(ack=ack))


class Batch(_BatchBase):
"""Accumulate mutations for transmission during :meth:`commit`."""
Expand Down
9 changes: 9 additions & 0 deletions packages/google-cloud-spanner/test_spanner.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this added by mistake?

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin
request = spanner_instance_admin.UpdateInstanceRequest(
instance=spanner_instance_admin.Instance(
name="projects/my-project/instances/my-instance",
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE,
),
field_mask={"paths": ["edition"]},
)
print("SUCCESS")
Comment on lines +1 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This file appears to be a temporary scratchpad or debug script that was accidentally committed. It should be removed from the repository.

36 changes: 33 additions & 3 deletions packages/google-cloud-spanner/tests/system/_async/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,12 +138,36 @@ async def shared_instance(
instance_config,
):
spanner_client._instance_admin_api = None
instance = spanner_client.instance(shared_instance_id, instance_config.name)

if _helpers.CREATE_INSTANCE:
op = await instance.create()
await op.result(instance_operation_timeout)
import time

from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin

create_time = str(int(time.time()))
labels = {"python-spanner-systests": "true", "created": create_time}

request = spanner_instance_admin.CreateInstanceRequest(
parent=spanner_client.project_name,
instance_id=shared_instance_id,
instance=spanner_instance_admin.Instance(
config=instance_config.name,
display_name=shared_instance_id,
node_count=1,
labels=labels,
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE_PLUS,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the ENTERPRISE_PLUS edition for system tests can significantly increase costs. Since Cloud Spanner Queues are also supported on the ENTERPRISE edition, consider using ENTERPRISE instead to minimize testing expenses.

Suggested change
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE_PLUS,
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE,

),
)
created_op = await spanner_client.instance_admin_api.create_instance(
request=request
)
await created_op.result(instance_operation_timeout)

instance = spanner_client.instance(
shared_instance_id, instance_config.name, labels=labels
)
else:
instance = spanner_client.instance(shared_instance_id, instance_config.name)
await instance.reload()

yield instance
Expand Down Expand Up @@ -205,3 +229,9 @@ async def databases_to_delete():
def not_postgres(database_dialect):
if database_dialect == DatabaseDialect.POSTGRESQL:
pytest.skip("Skip for Postgres")


@pytest.fixture(scope="function")
def not_emulator():
if _helpers.USE_EMULATOR:
pytest.skip(f"{_helpers.USE_EMULATOR_ENVVAR} set in environment.")
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,76 @@ async def test_db_batch_insert_then_db_snapshot_read(shared_database):
sd._check_rows_data(from_snap)


@pytest.mark.asyncio
async def test_db_batch_send_and_ack(
not_emulator, spanner_client, database_dialect, shared_instance
):
import uuid

from google.api_core.exceptions import GoogleAPIError, MethodNotImplemented

from google.cloud.spanner_admin_database_v1 import DatabaseDialect

db_name = f"test-db-{uuid.uuid4().hex[:8]}"
queue_name = f"test_queue_{uuid.uuid4().hex[:8]}"

test_database = await shared_instance.database(
db_name, database_dialect=database_dialect
)
operation = await test_database.create()
operation.result(300)

try:
# Create the Queue
if database_dialect == DatabaseDialect.POSTGRESQL:
queue_ddl = f"""CREATE QUEUE {queue_name} (
id bigint NOT NULL,
"Payload" varchar NOT NULL,
PRIMARY KEY (id)
)"""
else:
queue_ddl = f"""CREATE QUEUE {queue_name} (
Id INT64 NOT NULL,
Payload STRING(MAX) NOT NULL
) PRIMARY KEY (Id)"""

try:
operation = await test_database.update_ddl([queue_ddl])
await operation.result(600)
except MethodNotImplemented as e:
pytest.skip(f"Queues are not implemented yet: {e}")
except GoogleAPIError as e:
if (
getattr(e, "code", None) == 501
or (
getattr(e, "grpc_status_code", None)
and e.grpc_status_code.name == "UNIMPLEMENTED"
)
or "UNIMPLEMENTED" in str(e)
):
pytest.skip(f"Queues are not implemented yet: {e}")
raise
Comment on lines +128 to +138

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking e.grpc_status_code.name directly can raise an AttributeError if grpc_status_code is not a gRPC StatusCode enum (e.g., if it is an integer or None in some environments). Using getattr to safely retrieve the status code name is more robust and avoids potential runtime errors during tests.

        except GoogleAPIError as e:
            grpc_status_name = getattr(getattr(e, "grpc_status_code", None), "name", None)
            if (
                getattr(e, "code", None) == 501
                or grpc_status_name == "UNIMPLEMENTED"
                or "UNIMPLEMENTED" in str(e)
            ):
                pytest.skip(f"Queues are not implemented yet: {e}")
            raise


# Run mutations
async with test_database.batch() as batch:
batch.send(
queue=queue_name,
key=(2,),
payload="Hello, Queues!",
)

print("Acking message in queue...")
async with test_database.batch() as batch:
batch.ack(
queue=queue_name,
key=(2,),
)

finally:
print("Dropping database...")
await test_database.drop()


@pytest.mark.asyncio
async def test_db_run_in_transaction_then_snapshot_execute_sql(shared_database):
await shared_database.reload()
Expand Down
20 changes: 18 additions & 2 deletions packages/google-cloud-spanner/tests/system/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,30 @@ def shared_instance(
_helpers.cleanup_old_instances(spanner_client)

if _helpers.CREATE_INSTANCE:
from google.cloud.spanner_admin_instance_v1.types import spanner_instance_admin

create_time = str(int(time.time()))
labels = {"python-spanner-systests": "true", "created": create_time}

request = spanner_instance_admin.CreateInstanceRequest(
parent=spanner_client.project_name,
instance_id=shared_instance_id,
instance=spanner_instance_admin.Instance(
config=instance_config.name,
display_name=shared_instance_id,
node_count=1,
labels=labels,
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE_PLUS,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using the ENTERPRISE_PLUS edition for system tests can significantly increase costs. Since Cloud Spanner Queues are also supported on the ENTERPRISE edition, consider using ENTERPRISE instead to minimize testing expenses.

Suggested change
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE_PLUS,
edition=spanner_instance_admin.Instance.Edition.ENTERPRISE,

),
)
created_op = _helpers.retry_429_503(
spanner_client.instance_admin_api.create_instance
)(request=request)
created_op.result(instance_operation_timeout) # block until completion

instance = spanner_client.instance(
shared_instance_id, instance_config.name, labels=labels
)
created_op = _helpers.retry_429_503(instance.create)()
created_op.result(instance_operation_timeout) # block until completion

else: # reuse existing instance
instance = spanner_client.instance(shared_instance_id)
Expand Down
69 changes: 69 additions & 0 deletions packages/google-cloud-spanner/tests/system/test_database_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,75 @@ def test_db_batch_insert_then_db_snapshot_read(shared_database):
sd._check_rows_data(from_snap)


def test_db_batch_send_and_ack(
not_emulator, spanner_client, database_dialect, shared_instance
):
import uuid

from google.api_core.exceptions import GoogleAPIError, MethodNotImplemented

from google.cloud.spanner_admin_database_v1 import DatabaseDialect

db_id = f"test-db-{uuid.uuid4().hex[:8]}"
queue_name = f"test_queue_{uuid.uuid4().hex[:8]}"

test_database = shared_instance.database(db_id, database_dialect=database_dialect)
operation = test_database.create()
operation.result(300)
print("Database created successfully!")

try:
# Create the Queue
if database_dialect == DatabaseDialect.POSTGRESQL:
queue_ddl = f"""CREATE QUEUE {queue_name} (
id bigint NOT NULL,
"Payload" varchar NOT NULL,
PRIMARY KEY (id)
)"""
else:
queue_ddl = f"""CREATE QUEUE {queue_name} (
Id INT64 NOT NULL,
Payload STRING(MAX) NOT NULL
) PRIMARY KEY (Id)"""
try:
operation = test_database.update_ddl([queue_ddl])
operation.result(600)
except MethodNotImplemented as e:
pytest.skip(f"Queues are not implemented yet: {e}")
except GoogleAPIError as e:
if (
getattr(e, "code", None) == 501
or getattr(e, "grpc_status_code", None)
and e.grpc_status_code.name == "UNIMPLEMENTED"
or "UNIMPLEMENTED" in str(e)
):
pytest.skip(f"Queues are not implemented yet: {e}")
raise
Comment on lines +605 to +613

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking e.grpc_status_code.name directly can raise an AttributeError if grpc_status_code is not a gRPC StatusCode enum (e.g., if it is an integer or None in some environments). Using getattr to safely retrieve the status code name is more robust and avoids potential runtime errors during tests.

Suggested change
except GoogleAPIError as e:
if (
getattr(e, "code", None) == 501
or getattr(e, "grpc_status_code", None)
and e.grpc_status_code.name == "UNIMPLEMENTED"
or "UNIMPLEMENTED" in str(e)
):
pytest.skip(f"Queues are not implemented yet: {e}")
raise
except GoogleAPIError as e:
grpc_status_name = getattr(getattr(e, "grpc_status_code", None), "name", None)
if (
getattr(e, "code", None) == 501
or grpc_status_name == "UNIMPLEMENTED"
or "UNIMPLEMENTED" in str(e)
):
pytest.skip(f"Queues are not implemented yet: {e}")
raise

print("Queue created successfully.")

# Run mutations
print("Sending message to queue...")
with test_database.batch() as batch:
batch.send(
queue=queue_name,
key=(2,),
payload="Hello, Queues!",
)
print("Send successful.")

print("Acking message in queue...")
with test_database.batch() as batch:
batch.ack(
queue=queue_name,
key=(2,),
)
print("Ack successful.")

finally:
print("Dropping database...")
test_database.drop()


def test_db_run_in_transaction_then_snapshot_execute_sql(shared_database):
_helpers.retry_has_all_dll(shared_database.reload)()
sd = _sample_data
Expand Down
Loading
Loading