Skip to content

Commit 1c4aeca

Browse files
authored
chore(spanner): Add benchwrapper. (#657)
* chore(spanner): Add benchwrapper. * Add README.md to run the benchwrapper * Add README for regenerating protos for benchmarking * Small change to benchwrapper README * Update README.md for benchwrapper. * chore(spanner): Incorporate review comments. * chore(spanner): Incorporate review comments. * chore(spanner): accommodate review comments. - Add script docstring with usage instructions. - Modify copyright in spanner.proto.
1 parent 312435b commit 1c4aeca

File tree

9 files changed

+802
-0
lines changed

9 files changed

+802
-0
lines changed

benchmark/__init__.py

Whitespace-only changes.

benchmark/benchwrapper/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Benchwrapper
2+
3+
A small gRPC wrapper around the Spanner client library. This allows the
4+
benchmarking code to prod at Spanner without speaking Python.
5+
6+
## Running
7+
Run the following commands from python-spanner/ directory.
8+
```
9+
export SPANNER_EMULATOR_HOST=localhost:9010
10+
python3 -m benchmark.benchwrapper.main --port 8081

benchmark/benchwrapper/__init__.py

Whitespace-only changes.

benchmark/benchwrapper/main.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Copyright 2022 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""The gRPC Benchwrapper around Python Client Library.
16+
Usage:
17+
# Start the emulator using either docker or gcloud CLI.
18+
19+
# Set up instance and load data into database.
20+
21+
# Set up environment variables.
22+
$ export SPANNER_EMULATOR_HOST=localhost:9010
23+
24+
# Run the benchmark from python-spanner/ directory.
25+
$ python3 -m benchmark.benchwrapper.main --port 8081
26+
27+
"""
28+
29+
from concurrent import futures
30+
from optparse import OptionParser
31+
32+
import os
33+
34+
import benchmark.benchwrapper.proto.spanner_pb2 as spanner_messages
35+
import benchmark.benchwrapper.proto.spanner_pb2_grpc as spanner_service
36+
37+
from google.cloud import spanner
38+
39+
import grpc
40+
41+
################################## CONSTANTS ##################################
42+
43+
SPANNER_PROJECT = "someproject"
44+
SPANNER_INSTANCE = "someinstance"
45+
SPANNER_DATABASE = "somedatabase"
46+
47+
###############################################################################
48+
49+
50+
class SpannerBenchWrapperService(spanner_service.SpannerBenchWrapperServicer):
51+
"""Benchwrapper Servicer class to implement Read, Insert and Update
52+
methods.
53+
54+
:type project_id: str
55+
:param project_id: Spanner project.
56+
57+
:type instance_id: str
58+
:param instance_id: The ID of instance that owns the database.
59+
60+
:type database_id: str
61+
:param database_id: the ID of the database.
62+
"""
63+
64+
def __init__(self,
65+
project_id=SPANNER_PROJECT,
66+
instance_id=SPANNER_INSTANCE,
67+
database_id=SPANNER_DATABASE) -> None:
68+
69+
spanner_client = spanner.Client(project_id)
70+
instance = spanner_client.instance(instance_id)
71+
self.database = instance.database(database_id)
72+
73+
super().__init__()
74+
75+
def Read(self, request, _):
76+
"""Read represents operations like Go's ReadOnlyTransaction.Query,
77+
Java's ReadOnlyTransaction.executeQuery, Python's snapshot.read, and
78+
Node's Transaction.Read.
79+
80+
It will typically be used to read many items.
81+
82+
:type request:
83+
:class: `benchmark.benchwrapper.proto.spanner_pb2.ReadQuery`
84+
:param request: A ReadQuery request object.
85+
86+
:rtype: :class:`benchmark.benchwrapper.proto.spanner_pb2.EmptyResponse`
87+
:returns: An EmptyResponse object.
88+
"""
89+
with self.database.snapshot() as snapshot:
90+
# Stream the response to the query.
91+
list(snapshot.execute_sql(request.query))
92+
93+
return spanner_messages.EmptyResponse()
94+
95+
def Insert(self, request, _):
96+
"""Insert represents operations like Go's Client.Apply, Java's
97+
DatabaseClient.writeAtLeastOnce, Python's transaction.commit, and Node's
98+
Transaction.Commit.
99+
100+
It will typically be used to insert many items.
101+
102+
:type request:
103+
:class: `benchmark.benchwrapper.proto.spanner_pb2.InsertQuery`
104+
:param request: An InsertQuery request object.
105+
106+
:rtype: :class:`benchmark.benchwrapper.proto.spanner_pb2.EmptyResponse`
107+
:returns: An EmptyResponse object.
108+
"""
109+
with self.database.batch() as batch:
110+
batch.insert(
111+
table="Singers",
112+
columns=("SingerId", "FirstName", "LastName"),
113+
values=[(i.id, i.first_name, i.last_name) for i in request.singers],
114+
)
115+
116+
return spanner_messages.EmptyResponse()
117+
118+
def Update(self, request, _):
119+
"""Update represents operations like Go's
120+
ReadWriteTransaction.BatchUpdate, Java's TransactionRunner.run,
121+
Python's Batch.update, and Node's Transaction.BatchUpdate.
122+
123+
It will typically be used to update many items.
124+
125+
:type request:
126+
:class: `benchmark.benchwrapper.proto.spanner_pb2.UpdateQuery`
127+
:param request: An UpdateQuery request object.
128+
129+
:rtype: :class:`benchmark.benchwrapper.proto.spanner_pb2.EmptyResponse`
130+
:returns: An EmptyResponse object.
131+
"""
132+
self.database.run_in_transaction(self.update_singers, request.queries)
133+
134+
return spanner_messages.EmptyResponse()
135+
136+
def update_singers(self, transaction, stmts):
137+
"""Method to execute batch_update in a transaction.
138+
139+
:type transaction:
140+
:class: `google.cloud.spanner_v1.transaction.Transaction`
141+
:param transaction: A Spanner Transaction object.
142+
:type stmts:
143+
:class: `google.protobuf.pyext._message.RepeatedScalarContainer`
144+
:param stmts: Statements which are update queries.
145+
"""
146+
transaction.batch_update(stmts)
147+
148+
149+
def get_opts():
150+
"""Parse command line arguments."""
151+
parser = OptionParser()
152+
parser.add_option("-p", "--port", help="Specify a port to run on")
153+
154+
opts, _ = parser.parse_args()
155+
156+
return opts
157+
158+
159+
def validate_opts(opts):
160+
"""Validate command line arguments."""
161+
if opts.port is None:
162+
raise ValueError("Please specify a valid port, e.g., -p 5000 or "
163+
"--port 5000.")
164+
165+
166+
def start_grpc_server(num_workers, port):
167+
"""Method to start the GRPC server."""
168+
# Instantiate the GRPC server.
169+
server = grpc.server(futures.ThreadPoolExecutor(max_workers=num_workers))
170+
171+
# Instantiate benchwrapper service.
172+
spanner_benchwrapper_service = SpannerBenchWrapperService()
173+
174+
# Add benchwrapper servicer to server.
175+
spanner_service.add_SpannerBenchWrapperServicer_to_server(
176+
spanner_benchwrapper_service, server)
177+
178+
# Form the server address.
179+
addr = "localhost:{0}".format(port)
180+
181+
# Add the port, and start the server.
182+
server.add_insecure_port(addr)
183+
server.start()
184+
server.wait_for_termination()
185+
186+
187+
def serve():
188+
"""Driver method."""
189+
if "SPANNER_EMULATOR_HOST" not in os.environ:
190+
raise ValueError("This benchmarking server only works when connected "
191+
"to an emulator. Please set SPANNER_EMULATOR_HOST.")
192+
193+
opts = get_opts()
194+
195+
validate_opts(opts)
196+
197+
start_grpc_server(10, opts.port)
198+
199+
200+
if __name__ == "__main__":
201+
serve()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Regenerating protos
2+
Run the following command from python-spanner/ directory.
3+
```
4+
python3 -m grpc_tools.protoc -I . --python_out=. --grpc_python_out=. benchmark/benchwrapper/proto/*.proto

benchmark/benchwrapper/proto/__init__.py

Whitespace-only changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
syntax = "proto3";
16+
17+
package spanner_bench;
18+
19+
option py_generic_services = true;
20+
21+
message Singer {
22+
int64 id = 1;
23+
string first_name = 2;
24+
string last_name = 3;
25+
string singer_info = 4;
26+
}
27+
28+
message Album {
29+
int64 id = 1;
30+
int64 singer_id = 2;
31+
string album_title = 3;
32+
}
33+
34+
message ReadQuery {
35+
// The query to use in the read call.
36+
string query = 1;
37+
}
38+
39+
message InsertQuery {
40+
// The query to use in the insert call.
41+
repeated Singer singers = 1;
42+
repeated Album albums = 2;
43+
}
44+
45+
message UpdateQuery {
46+
// The queries to use in the update call.
47+
repeated string queries = 1;
48+
}
49+
50+
message EmptyResponse {}
51+
52+
service SpannerBenchWrapper {
53+
// Read represents operations like Go's ReadOnlyTransaction.Query, Java's
54+
// ReadOnlyTransaction.executeQuery, Python's snapshot.read, and Node's
55+
// Transaction.Read.
56+
//
57+
// It will typically be used to read many items.
58+
rpc Read(ReadQuery) returns (EmptyResponse) {}
59+
60+
// Insert represents operations like Go's Client.Apply, Java's
61+
// DatabaseClient.writeAtLeastOnce, Python's transaction.commit, and Node's
62+
// Transaction.Commit.
63+
//
64+
// It will typically be used to insert many items.
65+
rpc Insert(InsertQuery) returns (EmptyResponse) {}
66+
67+
// Update represents operations like Go's ReadWriteTransaction.BatchUpdate,
68+
// Java's TransactionRunner.run, Python's Batch.update, and Node's
69+
// Transaction.BatchUpdate.
70+
//
71+
// It will typically be used to update many items.
72+
rpc Update(UpdateQuery) returns (EmptyResponse) {}
73+
}

0 commit comments

Comments
 (0)