Skip to content

Commit 14fd3ff

Browse files
committed
perf: remove repeated GetSession calls for FixedSizePool
Add a _last_use_time to Session and use this to determine whether the FixedSizePool should check whether the session still exists, and whether it should be replaced. This significantly reduces the number of times that GetSession is called when using FixedSizePool.
1 parent ddb44a3 commit 14fd3ff

File tree

6 files changed

+69
-18
lines changed

6 files changed

+69
-18
lines changed

google/cloud/spanner_v1/pool.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@
2626
)
2727
from warnings import warn
2828

29-
_NOW = datetime.datetime.utcnow # unit tests may replace
29+
30+
def _now():
31+
datetime.datetime.now(datetime.UTC)
32+
33+
34+
_NOW = _now # unit tests may replace
3035

3136

3237
class AbstractSessionPool(object):
@@ -145,7 +150,8 @@ class FixedSizePool(AbstractSessionPool):
145150
- Pre-allocates / creates a fixed number of sessions.
146151
147152
- "Pings" existing sessions via :meth:`session.exists` before returning
148-
them, and replaces expired sessions.
153+
sessions that have not been used for more than 55 minutes and replaces
154+
expired sessions.
149155
150156
- Blocks, with a timeout, when :meth:`get` is called on an empty pool.
151157
Raises after timing out.
@@ -171,18 +177,21 @@ class FixedSizePool(AbstractSessionPool):
171177

172178
DEFAULT_SIZE = 10
173179
DEFAULT_TIMEOUT = 10
180+
DEFAULT_MAX_AGE_MINUTES = 55
174181

175182
def __init__(
176183
self,
177184
size=DEFAULT_SIZE,
178185
default_timeout=DEFAULT_TIMEOUT,
179186
labels=None,
180187
database_role=None,
188+
max_age_minutes=DEFAULT_MAX_AGE_MINUTES,
181189
):
182190
super(FixedSizePool, self).__init__(labels=labels, database_role=database_role)
183191
self.size = size
184192
self.default_timeout = default_timeout
185193
self._sessions = queue.LifoQueue(size)
194+
self._max_age = datetime.timedelta(minutes=max_age_minutes)
186195

187196
def bind(self, database):
188197
"""Associate the pool with a database.
@@ -230,8 +239,9 @@ def get(self, timeout=None):
230239
timeout = self.default_timeout
231240

232241
session = self._sessions.get(block=True, timeout=timeout)
242+
age = _NOW() - session.last_use_time
233243

234-
if not session.exists():
244+
if age >= self._max_age and not session.exists():
235245
session = self._database.session()
236246
session.create()
237247

google/cloud/spanner_v1/session.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from functools import total_ordering
1818
import random
1919
import time
20+
from datetime import datetime, UTC
2021

2122
from google.api_core.exceptions import Aborted
2223
from google.api_core.exceptions import GoogleAPICallError
@@ -69,6 +70,7 @@ def __init__(self, database, labels=None, database_role=None):
6970
labels = {}
7071
self._labels = labels
7172
self._database_role = database_role
73+
self._last_use_time = datetime.now(UTC)
7274

7375
def __lt__(self, other):
7476
return self._session_id < other._session_id
@@ -78,6 +80,14 @@ def session_id(self):
7880
"""Read-only ID, set by the back-end during :meth:`create`."""
7981
return self._session_id
8082

83+
@property
84+
def last_use_time(self):
85+
""" "Approximate last use time of this session
86+
87+
:rtype: datetime
88+
:returns: the approximate last use time of this session"""
89+
return self._last_use_time
90+
8191
@property
8292
def database_role(self):
8393
"""User-assigned database-role for the session.
@@ -154,6 +164,7 @@ def create(self):
154164
metadata=metadata,
155165
)
156166
self._session_id = session_pb.name.split("/")[-1]
167+
self._last_use_time = datetime.now()
157168

158169
def exists(self):
159170
"""Test for the existence of this session.
@@ -181,6 +192,7 @@ def exists(self):
181192
) as span:
182193
try:
183194
api.get_session(name=self.name, metadata=metadata)
195+
self._last_use_time = datetime.now()
184196
if span:
185197
span.set_attribute("session_found", True)
186198
except NotFound:
@@ -222,6 +234,7 @@ def ping(self):
222234
metadata = _metadata_with_prefix(self._database.name)
223235
request = ExecuteSqlRequest(session=self.name, sql="SELECT 1")
224236
api.execute_sql(request=request, metadata=metadata)
237+
self._last_use_time = datetime.now()
225238

226239
def snapshot(self, **kw):
227240
"""Create a snapshot to perform a set of reads with shared staleness.
@@ -273,6 +286,7 @@ def read(self, table, columns, keyset, index="", limit=0, column_info=None):
273286
:rtype: :class:`~google.cloud.spanner_v1.streamed.StreamedResultSet`
274287
:returns: a result set instance which can be used to consume rows.
275288
"""
289+
self._last_use_time = datetime.now()
276290
return self.snapshot().read(
277291
table, columns, keyset, index, limit, column_info=column_info
278292
)
@@ -339,6 +353,7 @@ def execute_sql(
339353
:rtype: :class:`~google.cloud.spanner_v1.streamed.StreamedResultSet`
340354
:returns: a result set instance which can be used to consume rows.
341355
"""
356+
self._last_use_time = datetime.now()
342357
return self.snapshot().execute_sql(
343358
sql,
344359
params,
@@ -378,6 +393,7 @@ def transaction(self):
378393
del self._transaction
379394

380395
txn = self._transaction = Transaction(self)
396+
self._last_use_time = datetime.now()
381397
return txn
382398

383399
def run_in_transaction(self, func, *args, **kw):
@@ -444,6 +460,7 @@ def run_in_transaction(self, func, *args, **kw):
444460
raise
445461

446462
try:
463+
self._last_use_time = datetime.now()
447464
txn.commit(
448465
return_commit_stats=self._database.log_commit_stats,
449466
request_options=commit_request_options,

google/cloud/spanner_v1/testing/mock_database_admin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from google.protobuf import empty_pb2
1717
import google.cloud.spanner_v1.testing.spanner_database_admin_pb2_grpc as database_admin_grpc
1818

19+
1920
# An in-memory mock DatabaseAdmin server that can be used for testing.
2021
class DatabaseAdminServicer(database_admin_grpc.DatabaseAdminServicer):
2122
def __init__(self):

google/cloud/spanner_v1/testing/mock_spanner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import google.cloud.spanner_v1.types.spanner as spanner
2525
import google.cloud.spanner_v1.types.transaction as transaction
2626

27+
2728
class MockSpanner:
2829
def __init__(self):
2930
self.results = {}

tests/mockserver_tests/test_basics.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
FixedSizePool,
3030
BatchCreateSessionsRequest,
3131
ExecuteSqlRequest,
32-
GetSessionRequest,
3332
)
3433
from google.cloud.spanner_v1.database import Database
3534
from google.cloud.spanner_v1.instance import Instance
@@ -125,12 +124,9 @@ def test_select1(self):
125124
self.assertEqual(1, row[0])
126125
self.assertEqual(1, len(result_list))
127126
requests = self.spanner_service.requests
128-
self.assertEqual(3, len(requests))
127+
self.assertEqual(2, len(requests), msg=requests)
129128
self.assertTrue(isinstance(requests[0], BatchCreateSessionsRequest))
130-
# TODO: Optimize FixedSizePool so this GetSessionRequest is not executed
131-
# every time a session is fetched.
132-
self.assertTrue(isinstance(requests[1], GetSessionRequest))
133-
self.assertTrue(isinstance(requests[2], ExecuteSqlRequest))
129+
self.assertTrue(isinstance(requests[1], ExecuteSqlRequest))
134130

135131
def test_create_table(self):
136132
database_admin_api = self.client.database_admin_api

tests/unit/test_pool.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
from functools import total_ordering
1717
import unittest
18+
from datetime import datetime, UTC, timedelta
1819

1920
import mock
2021

@@ -184,13 +185,30 @@ def test_bind(self):
184185
for session in SESSIONS:
185186
session.create.assert_not_called()
186187

187-
def test_get_non_expired(self):
188+
def test_get_active(self):
188189
pool = self._make_one(size=4)
189190
database = _Database("name")
190191
SESSIONS = sorted([_Session(database) for i in range(0, 4)])
191192
database._sessions.extend(SESSIONS)
192193
pool.bind(database)
193194

195+
# check if sessions returned in LIFO order
196+
for i in (3, 2, 1, 0):
197+
session = pool.get()
198+
self.assertIs(session, SESSIONS[i])
199+
self.assertFalse(session._exists_checked)
200+
self.assertFalse(pool._sessions.full())
201+
202+
def test_get_non_expired(self):
203+
pool = self._make_one(size=4)
204+
database = _Database("name")
205+
last_use_time = datetime.now(UTC) - timedelta(minutes=56)
206+
SESSIONS = sorted(
207+
[_Session(database, last_use_time=last_use_time) for i in range(0, 4)]
208+
)
209+
database._sessions.extend(SESSIONS)
210+
pool.bind(database)
211+
194212
# check if sessions returned in LIFO order
195213
for i in (3, 2, 1, 0):
196214
session = pool.get()
@@ -201,7 +219,8 @@ def test_get_non_expired(self):
201219
def test_get_expired(self):
202220
pool = self._make_one(size=4)
203221
database = _Database("name")
204-
SESSIONS = [_Session(database)] * 5
222+
last_use_time = datetime.now(UTC) - timedelta(minutes=65)
223+
SESSIONS = [_Session(database, last_use_time=last_use_time)] * 5
205224
SESSIONS[0]._exists = False
206225
database._sessions.extend(SESSIONS)
207226
pool.bind(database)
@@ -497,7 +516,7 @@ def test_get_hit_w_ping(self):
497516
SESSIONS = [_Session(database)] * 4
498517
database._sessions.extend(SESSIONS)
499518

500-
sessions_created = datetime.datetime.utcnow() - datetime.timedelta(seconds=4000)
519+
sessions_created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=4000)
501520

502521
with _Monkey(MUT, _NOW=lambda: sessions_created):
503522
pool.bind(database)
@@ -519,7 +538,7 @@ def test_get_hit_w_ping_expired(self):
519538
SESSIONS[0]._exists = False
520539
database._sessions.extend(SESSIONS)
521540

522-
sessions_created = datetime.datetime.utcnow() - datetime.timedelta(seconds=4000)
541+
sessions_created = datetime.datetime.now(UTC) - datetime.timedelta(seconds=4000)
523542

524543
with _Monkey(MUT, _NOW=lambda: sessions_created):
525544
pool.bind(database)
@@ -575,7 +594,7 @@ def test_put_non_full(self):
575594
pool = self._make_one(size=1)
576595
session_queue = pool._sessions = _Queue()
577596

578-
now = datetime.datetime.utcnow()
597+
now = datetime.datetime.now(UTC)
579598
database = _Database("name")
580599
session = _Session(database)
581600

@@ -631,7 +650,7 @@ def test_ping_oldest_stale_but_exists(self):
631650
database._sessions.extend(SESSIONS)
632651
pool.bind(database)
633652

634-
later = datetime.datetime.utcnow() + datetime.timedelta(seconds=4000)
653+
later = datetime.datetime.now(UTC) + datetime.timedelta(seconds=4000)
635654
with _Monkey(MUT, _NOW=lambda: later):
636655
pool.ping()
637656

@@ -649,7 +668,7 @@ def test_ping_oldest_stale_and_not_exists(self):
649668
database._sessions.extend(SESSIONS)
650669
pool.bind(database)
651670

652-
later = datetime.datetime.utcnow() + datetime.timedelta(seconds=4000)
671+
later = datetime.datetime.now(UTC) + datetime.timedelta(seconds=4000)
653672
with _Monkey(MUT, _NOW=lambda: later):
654673
pool.ping()
655674

@@ -733,7 +752,7 @@ def test_bind_w_timestamp_race(self):
733752
from google.cloud._testing import _Monkey
734753
from google.cloud.spanner_v1 import pool as MUT
735754

736-
NOW = datetime.datetime.utcnow()
755+
NOW = datetime.datetime.now(UTC)
737756
pool = self._make_one()
738757
database = _Database("name")
739758
SESSIONS = [_Session(database) for _ in range(10)]
@@ -915,18 +934,25 @@ def _make_transaction(*args, **kw):
915934
class _Session(object):
916935
_transaction = None
917936

918-
def __init__(self, database, exists=True, transaction=None):
937+
def __init__(
938+
self, database, exists=True, transaction=None, last_use_time=datetime.now(UTC)
939+
):
919940
self._database = database
920941
self._exists = exists
921942
self._exists_checked = False
922943
self._pinged = False
923944
self.create = mock.Mock()
924945
self._deleted = False
925946
self._transaction = transaction
947+
self._last_use_time = last_use_time
926948

927949
def __lt__(self, other):
928950
return id(self) < id(other)
929951

952+
@property
953+
def last_use_time(self):
954+
return self._last_use_time
955+
930956
def exists(self):
931957
self._exists_checked = True
932958
return self._exists

0 commit comments

Comments
 (0)