Skip to content

Commit 9569b39

Browse files
authored
perf(spanner): add fast-path for multiplexed session acquisition (#18317)
- Bypass initialization and multiplexed session locks on steady-state queries. - Build replacement sessions outside the lock during maintenance rotation, holding the lock only for the pointer swap. - Replace maintenance sleep loops with event wait for immediate shutdown termination. - Use monotonic time for maintenance intervals and clear local manager references before waiting. ### Results Summary | Metric | PR #18317 (`spanner-fast-path-mux-session-acquisition`) | 7-Day Nightly Baseline (`main`) | Delta | Absolute Difference | | :--- | :--- | :--- | :--- | :--- | | **Sample Count** | 628,791 ops | 300,566,193 ops | — | — | | **Mean Latency** | **5.327 ms** | 5.344 ms | **-0.32%** | -0.017 ms (-17 us) | | **P50 Latency** | **4.962 ms** | 4.996 ms | **-0.68%** | -0.034 ms (-34 us) | | **P90 Latency** | **6.624 ms** | 6.769 ms | **-2.15%** | -0.145 ms (-145 us) | | **P99 Latency** | **11.068 ms** | 10.700 ms | **+3.44%** | +0.368 ms | The performance gain from this optimization is minimal in an end-to-end test.
1 parent e66103d commit 9569b39

4 files changed

Lines changed: 903 additions & 54 deletions

File tree

packages/google-cloud-spanner/google/cloud/spanner_v1/_async/database_sessions_manager.py

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import asyncio
2020
import threading
21+
import time
2122
from datetime import timedelta
2223
from enum import Enum
2324
from os import getenv
@@ -128,6 +129,10 @@ async def _get_multiplexed_session(self) -> Session:
128129
129130
:rtype: :class:`~google.cloud.spanner_v1.session.Session`
130131
:returns: a multiplexed session."""
132+
session = self._multiplexed_session
133+
if session is not None:
134+
return session
135+
131136
with self._init_lock:
132137
if self._multiplexed_session_lock is None:
133138
self._multiplexed_session_lock = CrossSync.Lock()
@@ -136,10 +141,12 @@ async def _get_multiplexed_session(self) -> Session:
136141

137142
async with self._multiplexed_session_lock:
138143
if self._multiplexed_session is None:
139-
self._multiplexed_session = await self._build_multiplexed_session()
140-
self._multiplexed_session_thread = self._build_maintenance_thread()
144+
session = await self._build_multiplexed_session()
145+
maintenance_thread = self._build_maintenance_thread(session)
141146
if not CrossSync.is_async:
142-
self._multiplexed_session_thread.start()
147+
maintenance_thread.start()
148+
self._multiplexed_session_thread = maintenance_thread
149+
self._multiplexed_session = session
143150
return self._multiplexed_session
144151

145152
@CrossSync.convert
@@ -156,26 +163,63 @@ async def _build_multiplexed_session(self) -> Session:
156163
await session.create()
157164
return session
158165

159-
def _build_maintenance_thread(self) -> CrossSync.Task:
166+
def _build_maintenance_thread(
167+
self, session: Optional[Session] = None
168+
) -> CrossSync.Task:
160169
"""Builds and returns a multiplexed session maintenance thread for
161170
the database session manager. This thread will periodically delete
162171
and recreate the multiplexed session to ensure that it is always valid.
163172
173+
:type session: :class:`~google.cloud.spanner_v1.session.Session`
174+
:param session: (Optional) The multiplexed session to maintain.
175+
164176
:rtype: :class:`CrossSync.Task`
165177
:returns: a multiplexed session maintenance thread."""
178+
session_to_maintain = (
179+
session if session is not None else self._multiplexed_session
180+
)
166181
session_manager_ref = ref(self)
167182
if CrossSync.is_async:
168183
return CrossSync.create_task(
169184
self._maintain_multiplexed_session, session_manager_ref
170185
)
171186
else:
187+
session_id = (
188+
session_to_maintain.session_id
189+
if session_to_maintain is not None
190+
else ""
191+
)
172192
return Thread(
173193
target=self._maintain_multiplexed_session,
174-
name=f"maintenance-multiplexed-session-{self._multiplexed_session.session_id}",
194+
name=f"maintenance-multiplexed-session-{session_id}",
175195
args=[session_manager_ref],
176196
daemon=True,
177197
)
178198

199+
@CrossSync.convert
200+
async def _rotate_multiplexed_session(self) -> bool:
201+
"""Rotates the multiplexed session by building and swapping in a new session.
202+
203+
:rtype: bool
204+
:returns: True if the session was successfully refreshed, False otherwise.
205+
"""
206+
try:
207+
new_session = await self._build_multiplexed_session()
208+
except Exception:
209+
return False
210+
211+
async with self._multiplexed_session_lock:
212+
old_session = self._multiplexed_session
213+
self._multiplexed_session = new_session
214+
215+
if old_session is not None:
216+
try:
217+
await CrossSync.run_if_async(old_session.delete)
218+
except Exception:
219+
pass
220+
221+
return True
222+
179223
@staticmethod
180224
@CrossSync.convert
181225
async def _maintain_multiplexed_session(session_manager_ref) -> None:
@@ -196,24 +240,26 @@ async def _maintain_multiplexed_session(session_manager_ref) -> None:
196240
refresh_interval_seconds = (
197241
manager._MAINTENANCE_THREAD_REFRESH_INTERVAL.total_seconds()
198242
)
199-
from time import time
200-
201-
session_created_time = time()
243+
session_created_time = time.monotonic()
202244
while True:
203245
manager = session_manager_ref()
204246
if manager is None:
205247
return
206-
if manager._multiplexed_session_terminate_event.is_set():
248+
terminate_event = manager._multiplexed_session_terminate_event
249+
if terminate_event.is_set():
207250
return
208-
if time() - session_created_time < refresh_interval_seconds:
209-
await CrossSync.sleep(polling_interval_seconds)
210-
continue
211-
async with manager._multiplexed_session_lock:
212-
await CrossSync.run_if_async(manager._multiplexed_session.delete)
213-
manager._multiplexed_session = (
214-
await manager._build_multiplexed_session()
215-
)
216-
session_created_time = time()
251+
252+
if time.monotonic() - session_created_time >= refresh_interval_seconds:
253+
if await manager._rotate_multiplexed_session():
254+
session_created_time = time.monotonic()
255+
manager = None
256+
continue
257+
258+
manager = None
259+
await CrossSync.event_wait(
260+
terminate_event,
261+
timeout=polling_interval_seconds,
262+
)
217263

218264
@classmethod
219265
def _use_multiplexed(cls, transaction_type: TransactionType) -> bool:
@@ -247,5 +293,6 @@ async def close(self) -> None:
247293
else:
248294
self._multiplexed_session_thread.join()
249295
if self._multiplexed_session is not None:
250-
await self._multiplexed_session.delete()
296+
session_to_delete = self._multiplexed_session
251297
self._multiplexed_session = None
298+
await session_to_delete.delete()

packages/google-cloud-spanner/google/cloud/spanner_v1/database_sessions_manager.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"""Manage sessions for a database."""
1919

2020
import threading
21+
import time
2122
from datetime import timedelta
2223
from enum import Enum
2324
from os import getenv
@@ -126,16 +127,22 @@ def _get_multiplexed_session(self) -> Session:
126127
127128
:rtype: :class:`~google.cloud.spanner_v1.session.Session`
128129
:returns: a multiplexed session."""
130+
session = self._multiplexed_session
131+
if session is not None:
132+
return session
133+
129134
with self._init_lock:
130135
if self._multiplexed_session_lock is None:
131136
self._multiplexed_session_lock = CrossSync._Sync_Impl.Lock()
132137
if self._multiplexed_session_terminate_event is None:
133138
self._multiplexed_session_terminate_event = CrossSync._Sync_Impl.Event()
134139
with self._multiplexed_session_lock:
135140
if self._multiplexed_session is None:
136-
self._multiplexed_session = self._build_multiplexed_session()
137-
self._multiplexed_session_thread = self._build_maintenance_thread()
138-
self._multiplexed_session_thread.start()
141+
session = self._build_multiplexed_session()
142+
maintenance_thread = self._build_maintenance_thread(session)
143+
maintenance_thread.start()
144+
self._multiplexed_session_thread = maintenance_thread
145+
self._multiplexed_session = session
139146
return self._multiplexed_session
140147

141148
def _build_multiplexed_session(self) -> Session:
@@ -151,21 +158,55 @@ def _build_multiplexed_session(self) -> Session:
151158
session.create()
152159
return session
153160

154-
def _build_maintenance_thread(self) -> CrossSync._Sync_Impl.Task:
161+
def _build_maintenance_thread(
162+
self, session: Optional[Session] = None
163+
) -> CrossSync._Sync_Impl.Task:
155164
"""Builds and returns a multiplexed session maintenance thread for
156165
the database session manager. This thread will periodically delete
157166
and recreate the multiplexed session to ensure that it is always valid.
158167
168+
:type session: :class:`~google.cloud.spanner_v1.session.Session`
169+
:param session: (Optional) The multiplexed session to maintain.
170+
159171
:rtype: :class:`CrossSync._Sync_Impl.Task`
160172
:returns: a multiplexed session maintenance thread."""
173+
session_to_maintain = (
174+
session if session is not None else self._multiplexed_session
175+
)
161176
session_manager_ref = ref(self)
177+
session_id = (
178+
session_to_maintain.session_id if session_to_maintain is not None else ""
179+
)
162180
return Thread(
163181
target=self._maintain_multiplexed_session,
164-
name=f"maintenance-multiplexed-session-{self._multiplexed_session.session_id}",
182+
name=f"maintenance-multiplexed-session-{session_id}",
165183
args=[session_manager_ref],
166184
daemon=True,
167185
)
168186

187+
def _rotate_multiplexed_session(self) -> bool:
188+
"""Rotates the multiplexed session by building and swapping in a new session.
189+
190+
:rtype: bool
191+
:returns: True if the session was successfully refreshed, False otherwise.
192+
"""
193+
try:
194+
new_session = self._build_multiplexed_session()
195+
except Exception:
196+
return False
197+
198+
with self._multiplexed_session_lock:
199+
old_session = self._multiplexed_session
200+
self._multiplexed_session = new_session
201+
202+
if old_session is not None:
203+
try:
204+
CrossSync._Sync_Impl.run_if_async(old_session.delete)
205+
except Exception:
206+
pass
207+
208+
return True
209+
169210
@staticmethod
170211
def _maintain_multiplexed_session(session_manager_ref) -> None:
171212
"""Maintains the multiplexed session for the database session manager.
@@ -185,22 +226,25 @@ def _maintain_multiplexed_session(session_manager_ref) -> None:
185226
refresh_interval_seconds = (
186227
manager._MAINTENANCE_THREAD_REFRESH_INTERVAL.total_seconds()
187228
)
188-
from time import time
189-
190-
session_created_time = time()
229+
session_created_time = time.monotonic()
191230
while True:
192231
manager = session_manager_ref()
193232
if manager is None:
194233
return
195-
if manager._multiplexed_session_terminate_event.is_set():
234+
terminate_event = manager._multiplexed_session_terminate_event
235+
if terminate_event.is_set():
196236
return
197-
if time() - session_created_time < refresh_interval_seconds:
198-
CrossSync._Sync_Impl.sleep(polling_interval_seconds)
199-
continue
200-
with manager._multiplexed_session_lock:
201-
CrossSync._Sync_Impl.run_if_async(manager._multiplexed_session.delete)
202-
manager._multiplexed_session = manager._build_multiplexed_session()
203-
session_created_time = time()
237+
if time.monotonic() - session_created_time >= refresh_interval_seconds:
238+
if manager._rotate_multiplexed_session():
239+
session_created_time = time.monotonic()
240+
manager = None
241+
continue
242+
243+
manager = None
244+
CrossSync._Sync_Impl.event_wait(
245+
terminate_event,
246+
timeout=polling_interval_seconds,
247+
)
204248

205249
@classmethod
206250
def _use_multiplexed(cls, transaction_type: TransactionType) -> bool:
@@ -226,5 +270,6 @@ def close(self) -> None:
226270
if self._multiplexed_session_thread is not None:
227271
self._multiplexed_session_thread.join()
228272
if self._multiplexed_session is not None:
229-
self._multiplexed_session.delete()
273+
session_to_delete = self._multiplexed_session
230274
self._multiplexed_session = None
275+
session_to_delete.delete()

0 commit comments

Comments
 (0)