google-cloud-spanner 3.55.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. google/cloud/spanner.py +47 -0
  2. google/cloud/spanner_admin_database_v1/__init__.py +146 -0
  3. google/cloud/spanner_admin_database_v1/gapic_metadata.json +418 -0
  4. google/cloud/spanner_admin_database_v1/gapic_version.py +16 -0
  5. google/cloud/spanner_admin_database_v1/py.typed +2 -0
  6. google/cloud/spanner_admin_database_v1/services/__init__.py +15 -0
  7. google/cloud/spanner_admin_database_v1/services/database_admin/__init__.py +22 -0
  8. google/cloud/spanner_admin_database_v1/services/database_admin/async_client.py +4097 -0
  9. google/cloud/spanner_admin_database_v1/services/database_admin/client.py +4602 -0
  10. google/cloud/spanner_admin_database_v1/services/database_admin/pagers.py +989 -0
  11. google/cloud/spanner_admin_database_v1/services/database_admin/transports/__init__.py +38 -0
  12. google/cloud/spanner_admin_database_v1/services/database_admin/transports/base.py +820 -0
  13. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc.py +1303 -0
  14. google/cloud/spanner_admin_database_v1/services/database_admin/transports/grpc_asyncio.py +1688 -0
  15. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest.py +6512 -0
  16. google/cloud/spanner_admin_database_v1/services/database_admin/transports/rest_base.py +1650 -0
  17. google/cloud/spanner_admin_database_v1/types/__init__.py +144 -0
  18. google/cloud/spanner_admin_database_v1/types/backup.py +1106 -0
  19. google/cloud/spanner_admin_database_v1/types/backup_schedule.py +369 -0
  20. google/cloud/spanner_admin_database_v1/types/common.py +180 -0
  21. google/cloud/spanner_admin_database_v1/types/spanner_database_admin.py +1303 -0
  22. google/cloud/spanner_admin_instance_v1/__init__.py +110 -0
  23. google/cloud/spanner_admin_instance_v1/gapic_metadata.json +343 -0
  24. google/cloud/spanner_admin_instance_v1/gapic_version.py +16 -0
  25. google/cloud/spanner_admin_instance_v1/py.typed +2 -0
  26. google/cloud/spanner_admin_instance_v1/services/__init__.py +15 -0
  27. google/cloud/spanner_admin_instance_v1/services/instance_admin/__init__.py +22 -0
  28. google/cloud/spanner_admin_instance_v1/services/instance_admin/async_client.py +3466 -0
  29. google/cloud/spanner_admin_instance_v1/services/instance_admin/client.py +3881 -0
  30. google/cloud/spanner_admin_instance_v1/services/instance_admin/pagers.py +856 -0
  31. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/__init__.py +38 -0
  32. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/base.py +545 -0
  33. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc.py +1347 -0
  34. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/grpc_asyncio.py +1539 -0
  35. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest.py +4834 -0
  36. google/cloud/spanner_admin_instance_v1/services/instance_admin/transports/rest_base.py +1198 -0
  37. google/cloud/spanner_admin_instance_v1/types/__init__.py +104 -0
  38. google/cloud/spanner_admin_instance_v1/types/common.py +99 -0
  39. google/cloud/spanner_admin_instance_v1/types/spanner_instance_admin.py +2375 -0
  40. google/cloud/spanner_dbapi/__init__.py +93 -0
  41. google/cloud/spanner_dbapi/_helpers.py +113 -0
  42. google/cloud/spanner_dbapi/batch_dml_executor.py +135 -0
  43. google/cloud/spanner_dbapi/checksum.py +80 -0
  44. google/cloud/spanner_dbapi/client_side_statement_executor.py +140 -0
  45. google/cloud/spanner_dbapi/client_side_statement_parser.py +106 -0
  46. google/cloud/spanner_dbapi/connection.py +818 -0
  47. google/cloud/spanner_dbapi/cursor.py +609 -0
  48. google/cloud/spanner_dbapi/exceptions.py +172 -0
  49. google/cloud/spanner_dbapi/parse_utils.py +392 -0
  50. google/cloud/spanner_dbapi/parsed_statement.py +63 -0
  51. google/cloud/spanner_dbapi/parser.py +258 -0
  52. google/cloud/spanner_dbapi/partition_helper.py +41 -0
  53. google/cloud/spanner_dbapi/transaction_helper.py +294 -0
  54. google/cloud/spanner_dbapi/types.py +106 -0
  55. google/cloud/spanner_dbapi/utils.py +147 -0
  56. google/cloud/spanner_dbapi/version.py +20 -0
  57. google/cloud/spanner_v1/__init__.py +154 -0
  58. google/cloud/spanner_v1/_helpers.py +751 -0
  59. google/cloud/spanner_v1/_opentelemetry_tracing.py +165 -0
  60. google/cloud/spanner_v1/backup.py +397 -0
  61. google/cloud/spanner_v1/batch.py +433 -0
  62. google/cloud/spanner_v1/client.py +538 -0
  63. google/cloud/spanner_v1/data_types.py +350 -0
  64. google/cloud/spanner_v1/database.py +1968 -0
  65. google/cloud/spanner_v1/database_sessions_manager.py +249 -0
  66. google/cloud/spanner_v1/gapic_metadata.json +268 -0
  67. google/cloud/spanner_v1/gapic_version.py +16 -0
  68. google/cloud/spanner_v1/instance.py +735 -0
  69. google/cloud/spanner_v1/keyset.py +193 -0
  70. google/cloud/spanner_v1/merged_result_set.py +146 -0
  71. google/cloud/spanner_v1/metrics/constants.py +71 -0
  72. google/cloud/spanner_v1/metrics/metrics_capture.py +75 -0
  73. google/cloud/spanner_v1/metrics/metrics_exporter.py +384 -0
  74. google/cloud/spanner_v1/metrics/metrics_interceptor.py +156 -0
  75. google/cloud/spanner_v1/metrics/metrics_tracer.py +588 -0
  76. google/cloud/spanner_v1/metrics/metrics_tracer_factory.py +328 -0
  77. google/cloud/spanner_v1/metrics/spanner_metrics_tracer_factory.py +172 -0
  78. google/cloud/spanner_v1/param_types.py +110 -0
  79. google/cloud/spanner_v1/pool.py +813 -0
  80. google/cloud/spanner_v1/py.typed +2 -0
  81. google/cloud/spanner_v1/request_id_header.py +64 -0
  82. google/cloud/spanner_v1/services/__init__.py +15 -0
  83. google/cloud/spanner_v1/services/spanner/__init__.py +22 -0
  84. google/cloud/spanner_v1/services/spanner/async_client.py +2205 -0
  85. google/cloud/spanner_v1/services/spanner/client.py +2624 -0
  86. google/cloud/spanner_v1/services/spanner/pagers.py +196 -0
  87. google/cloud/spanner_v1/services/spanner/transports/__init__.py +38 -0
  88. google/cloud/spanner_v1/services/spanner/transports/base.py +520 -0
  89. google/cloud/spanner_v1/services/spanner/transports/grpc.py +911 -0
  90. google/cloud/spanner_v1/services/spanner/transports/grpc_asyncio.py +1144 -0
  91. google/cloud/spanner_v1/services/spanner/transports/rest.py +3468 -0
  92. google/cloud/spanner_v1/services/spanner/transports/rest_base.py +981 -0
  93. google/cloud/spanner_v1/session.py +631 -0
  94. google/cloud/spanner_v1/session_options.py +133 -0
  95. google/cloud/spanner_v1/snapshot.py +1057 -0
  96. google/cloud/spanner_v1/streamed.py +402 -0
  97. google/cloud/spanner_v1/table.py +181 -0
  98. google/cloud/spanner_v1/testing/__init__.py +0 -0
  99. google/cloud/spanner_v1/testing/database_test.py +121 -0
  100. google/cloud/spanner_v1/testing/interceptors.py +118 -0
  101. google/cloud/spanner_v1/testing/mock_database_admin.py +38 -0
  102. google/cloud/spanner_v1/testing/mock_spanner.py +261 -0
  103. google/cloud/spanner_v1/testing/spanner_database_admin_pb2_grpc.py +1267 -0
  104. google/cloud/spanner_v1/testing/spanner_pb2_grpc.py +882 -0
  105. google/cloud/spanner_v1/transaction.py +747 -0
  106. google/cloud/spanner_v1/types/__init__.py +118 -0
  107. google/cloud/spanner_v1/types/commit_response.py +94 -0
  108. google/cloud/spanner_v1/types/keys.py +248 -0
  109. google/cloud/spanner_v1/types/mutation.py +201 -0
  110. google/cloud/spanner_v1/types/query_plan.py +220 -0
  111. google/cloud/spanner_v1/types/result_set.py +379 -0
  112. google/cloud/spanner_v1/types/spanner.py +1815 -0
  113. google/cloud/spanner_v1/types/transaction.py +818 -0
  114. google/cloud/spanner_v1/types/type.py +288 -0
  115. google_cloud_spanner-3.55.0.dist-info/LICENSE +202 -0
  116. google_cloud_spanner-3.55.0.dist-info/METADATA +318 -0
  117. google_cloud_spanner-3.55.0.dist-info/RECORD +119 -0
  118. google_cloud_spanner-3.55.0.dist-info/WHEEL +5 -0
  119. google_cloud_spanner-3.55.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,813 @@
1
+ # Copyright 2016 Google LLC All rights reserved.
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
+ """Pools managing shared Session objects."""
16
+
17
+ import datetime
18
+ import queue
19
+ import time
20
+
21
+ from google.cloud.exceptions import NotFound
22
+ from google.cloud.spanner_v1 import BatchCreateSessionsRequest
23
+ from google.cloud.spanner_v1 import Session
24
+ from google.cloud.spanner_v1._helpers import (
25
+ _metadata_with_prefix,
26
+ _metadata_with_leader_aware_routing,
27
+ )
28
+ from google.cloud.spanner_v1._opentelemetry_tracing import (
29
+ add_span_event,
30
+ get_current_span,
31
+ trace_call,
32
+ )
33
+ from warnings import warn
34
+
35
+ from google.cloud.spanner_v1.metrics.metrics_capture import MetricsCapture
36
+
37
+ _NOW = datetime.datetime.utcnow # unit tests may replace
38
+
39
+
40
+ class AbstractSessionPool(object):
41
+ """Specifies required API for concrete session pool implementations.
42
+
43
+ :type labels: dict (str -> str) or None
44
+ :param labels: (Optional) user-assigned labels for sessions created
45
+ by the pool.
46
+
47
+ :type database_role: str
48
+ :param database_role: (Optional) user-assigned database_role for the session.
49
+ """
50
+
51
+ _database = None
52
+
53
+ def __init__(self, labels=None, database_role=None):
54
+ if labels is None:
55
+ labels = {}
56
+ self._labels = labels
57
+ self._database_role = database_role
58
+
59
+ @property
60
+ def labels(self):
61
+ """User-assigned labels for sessions created by the pool.
62
+
63
+ :rtype: dict (str -> str)
64
+ :returns: labels assigned by the user
65
+ """
66
+ return self._labels
67
+
68
+ @property
69
+ def database_role(self):
70
+ """User-assigned database_role for sessions created by the pool.
71
+
72
+ :rtype: str
73
+ :returns: database_role assigned by the user
74
+ """
75
+ return self._database_role
76
+
77
+ def bind(self, database):
78
+ """Associate the pool with a database.
79
+
80
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
81
+ :param database: database used by the pool to create sessions
82
+ when needed.
83
+
84
+ Concrete implementations of this method may pre-fill the pool
85
+ using the database.
86
+
87
+ :raises NotImplementedError: abstract method
88
+ """
89
+ raise NotImplementedError()
90
+
91
+ def get(self):
92
+ """Check a session out from the pool.
93
+
94
+ Concrete implementations of this method are allowed to raise an
95
+ error to signal that the pool is exhausted, or to block until a
96
+ session is available.
97
+
98
+ :raises NotImplementedError: abstract method
99
+ """
100
+ raise NotImplementedError()
101
+
102
+ def put(self, session):
103
+ """Return a session to the pool.
104
+
105
+ :type session: :class:`~google.cloud.spanner_v1.session.Session`
106
+ :param session: the session being returned.
107
+
108
+ Concrete implementations of this method are allowed to raise an
109
+ error to signal that the pool is full, or to block until it is
110
+ not full.
111
+
112
+ :raises NotImplementedError: abstract method
113
+ """
114
+ raise NotImplementedError()
115
+
116
+ def clear(self):
117
+ """Delete all sessions in the pool.
118
+
119
+ Concrete implementations of this method are allowed to raise an
120
+ error to signal that the pool is full, or to block until it is
121
+ not full.
122
+
123
+ :raises NotImplementedError: abstract method
124
+ """
125
+ raise NotImplementedError()
126
+
127
+ def _new_session(self):
128
+ """Helper for concrete methods creating session instances.
129
+
130
+ :rtype: :class:`~google.cloud.spanner_v1.session.Session`
131
+ :returns: new session instance.
132
+ """
133
+ return self._database.session(
134
+ labels=self.labels, database_role=self.database_role
135
+ )
136
+
137
+ def session(self, **kwargs):
138
+ """Check out a session from the pool.
139
+
140
+ :param kwargs: (optional) keyword arguments, passed through to
141
+ the returned checkout.
142
+
143
+ :rtype: :class:`~google.cloud.spanner_v1.session.SessionCheckout`
144
+ :returns: a checkout instance, to be used as a context manager for
145
+ accessing the session and returning it to the pool.
146
+ """
147
+ return SessionCheckout(self, **kwargs)
148
+
149
+
150
+ class FixedSizePool(AbstractSessionPool):
151
+ """Concrete session pool implementation:
152
+
153
+ - Pre-allocates / creates a fixed number of sessions.
154
+
155
+ - "Pings" existing sessions via :meth:`session.exists` before returning
156
+ sessions that have not been used for more than 55 minutes and replaces
157
+ expired sessions.
158
+
159
+ - Blocks, with a timeout, when :meth:`get` is called on an empty pool.
160
+ Raises after timing out.
161
+
162
+ - Raises when :meth:`put` is called on a full pool. That error is
163
+ never expected in normal practice, as users should be calling
164
+ :meth:`get` followed by :meth:`put` whenever in need of a session.
165
+
166
+ :type size: int
167
+ :param size: fixed pool size
168
+
169
+ :type default_timeout: int
170
+ :param default_timeout: default timeout, in seconds, to wait for
171
+ a returned session.
172
+
173
+ :type labels: dict (str -> str) or None
174
+ :param labels: (Optional) user-assigned labels for sessions created
175
+ by the pool.
176
+
177
+ :type database_role: str
178
+ :param database_role: (Optional) user-assigned database_role for the session.
179
+ """
180
+
181
+ DEFAULT_SIZE = 10
182
+ DEFAULT_TIMEOUT = 10
183
+ DEFAULT_MAX_AGE_MINUTES = 55
184
+
185
+ def __init__(
186
+ self,
187
+ size=DEFAULT_SIZE,
188
+ default_timeout=DEFAULT_TIMEOUT,
189
+ labels=None,
190
+ database_role=None,
191
+ max_age_minutes=DEFAULT_MAX_AGE_MINUTES,
192
+ ):
193
+ super(FixedSizePool, self).__init__(labels=labels, database_role=database_role)
194
+ self.size = size
195
+ self.default_timeout = default_timeout
196
+ self._sessions = queue.LifoQueue(size)
197
+ self._max_age = datetime.timedelta(minutes=max_age_minutes)
198
+
199
+ def bind(self, database):
200
+ """Associate the pool with a database.
201
+
202
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
203
+ :param database: database used by the pool to used to create sessions
204
+ when needed.
205
+ """
206
+ self._database = database
207
+ requested_session_count = self.size - self._sessions.qsize()
208
+ span = get_current_span()
209
+ span_event_attributes = {"kind": type(self).__name__}
210
+
211
+ if requested_session_count <= 0:
212
+ add_span_event(
213
+ span,
214
+ f"Invalid session pool size({requested_session_count}) <= 0",
215
+ span_event_attributes,
216
+ )
217
+ return
218
+
219
+ api = database.spanner_api
220
+ metadata = _metadata_with_prefix(database.name)
221
+ if database._route_to_leader_enabled:
222
+ metadata.append(
223
+ _metadata_with_leader_aware_routing(database._route_to_leader_enabled)
224
+ )
225
+ self._database_role = self._database_role or self._database.database_role
226
+ if requested_session_count > 0:
227
+ add_span_event(
228
+ span,
229
+ f"Requesting {requested_session_count} sessions",
230
+ span_event_attributes,
231
+ )
232
+
233
+ if self._sessions.full():
234
+ add_span_event(span, "Session pool is already full", span_event_attributes)
235
+ return
236
+
237
+ request = BatchCreateSessionsRequest(
238
+ database=database.name,
239
+ session_count=requested_session_count,
240
+ session_template=Session(creator_role=self.database_role),
241
+ )
242
+
243
+ observability_options = getattr(self._database, "observability_options", None)
244
+ with trace_call(
245
+ "CloudSpanner.FixedPool.BatchCreateSessions",
246
+ observability_options=observability_options,
247
+ metadata=metadata,
248
+ ) as span, MetricsCapture():
249
+ returned_session_count = 0
250
+ while not self._sessions.full():
251
+ request.session_count = requested_session_count - self._sessions.qsize()
252
+ add_span_event(
253
+ span,
254
+ f"Creating {request.session_count} sessions",
255
+ span_event_attributes,
256
+ )
257
+ resp = api.batch_create_sessions(
258
+ request=request,
259
+ metadata=database.metadata_with_request_id(
260
+ database._next_nth_request,
261
+ 1,
262
+ metadata,
263
+ span,
264
+ ),
265
+ )
266
+
267
+ add_span_event(
268
+ span,
269
+ "Created sessions",
270
+ dict(count=len(resp.session)),
271
+ )
272
+
273
+ for session_pb in resp.session:
274
+ session = self._new_session()
275
+ session._session_id = session_pb.name.split("/")[-1]
276
+ self._sessions.put(session)
277
+ returned_session_count += 1
278
+
279
+ add_span_event(
280
+ span,
281
+ f"Requested for {requested_session_count} sessions, returned {returned_session_count}",
282
+ span_event_attributes,
283
+ )
284
+
285
+ def get(self, timeout=None):
286
+ """Check a session out from the pool.
287
+
288
+ :type timeout: int
289
+ :param timeout: seconds to block waiting for an available session
290
+
291
+ :rtype: :class:`~google.cloud.spanner_v1.session.Session`
292
+ :returns: an existing session from the pool, or a newly-created
293
+ session.
294
+ :raises: :exc:`queue.Empty` if the queue is empty.
295
+ """
296
+ if timeout is None:
297
+ timeout = self.default_timeout
298
+
299
+ start_time = time.time()
300
+ current_span = get_current_span()
301
+ span_event_attributes = {"kind": type(self).__name__}
302
+ add_span_event(current_span, "Acquiring session", span_event_attributes)
303
+
304
+ session = None
305
+ try:
306
+ add_span_event(
307
+ current_span,
308
+ "Waiting for a session to become available",
309
+ span_event_attributes,
310
+ )
311
+
312
+ session = self._sessions.get(block=True, timeout=timeout)
313
+ age = _NOW() - session.last_use_time
314
+
315
+ if age >= self._max_age and not session.exists():
316
+ if not session.exists():
317
+ add_span_event(
318
+ current_span,
319
+ "Session is not valid, recreating it",
320
+ span_event_attributes,
321
+ )
322
+ session = self._database.session()
323
+ session.create()
324
+ # Replacing with the updated session.id.
325
+ span_event_attributes["session.id"] = session._session_id
326
+
327
+ span_event_attributes["session.id"] = session._session_id
328
+ span_event_attributes["time.elapsed"] = time.time() - start_time
329
+ add_span_event(current_span, "Acquired session", span_event_attributes)
330
+
331
+ except queue.Empty as e:
332
+ add_span_event(
333
+ current_span, "No sessions available in the pool", span_event_attributes
334
+ )
335
+ raise e
336
+
337
+ return session
338
+
339
+ def put(self, session):
340
+ """Return a session to the pool.
341
+
342
+ Never blocks: if the pool is full, raises.
343
+
344
+ :type session: :class:`~google.cloud.spanner_v1.session.Session`
345
+ :param session: the session being returned.
346
+
347
+ :raises: :exc:`queue.Full` if the queue is full.
348
+ """
349
+ self._sessions.put_nowait(session)
350
+
351
+ def clear(self):
352
+ """Delete all sessions in the pool."""
353
+
354
+ while True:
355
+ try:
356
+ session = self._sessions.get(block=False)
357
+ except queue.Empty:
358
+ break
359
+ else:
360
+ session.delete()
361
+
362
+
363
+ class BurstyPool(AbstractSessionPool):
364
+ """Concrete session pool implementation:
365
+
366
+ - "Pings" existing sessions via :meth:`session.exists` before returning
367
+ them.
368
+
369
+ - Creates a new session, rather than blocking, when :meth:`get` is called
370
+ on an empty pool.
371
+
372
+ - Discards the returned session, rather than blocking, when :meth:`put`
373
+ is called on a full pool.
374
+
375
+ :type target_size: int
376
+ :param target_size: max pool size
377
+
378
+ :type labels: dict (str -> str) or None
379
+ :param labels: (Optional) user-assigned labels for sessions created
380
+ by the pool.
381
+
382
+ :type database_role: str
383
+ :param database_role: (Optional) user-assigned database_role for the session.
384
+ """
385
+
386
+ def __init__(self, target_size=10, labels=None, database_role=None):
387
+ super(BurstyPool, self).__init__(labels=labels, database_role=database_role)
388
+ self.target_size = target_size
389
+ self._database = None
390
+ self._sessions = queue.LifoQueue(target_size)
391
+
392
+ def bind(self, database):
393
+ """Associate the pool with a database.
394
+
395
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
396
+ :param database: database used by the pool to create sessions
397
+ when needed.
398
+ """
399
+ self._database = database
400
+ self._database_role = self._database_role or self._database.database_role
401
+
402
+ def get(self):
403
+ """Check a session out from the pool.
404
+
405
+ :rtype: :class:`~google.cloud.spanner_v1.session.Session`
406
+ :returns: an existing session from the pool, or a newly-created
407
+ session.
408
+ """
409
+ current_span = get_current_span()
410
+ span_event_attributes = {"kind": type(self).__name__}
411
+ add_span_event(current_span, "Acquiring session", span_event_attributes)
412
+
413
+ try:
414
+ add_span_event(
415
+ current_span,
416
+ "Waiting for a session to become available",
417
+ span_event_attributes,
418
+ )
419
+ session = self._sessions.get_nowait()
420
+ except queue.Empty:
421
+ add_span_event(
422
+ current_span,
423
+ "No sessions available in pool. Creating session",
424
+ span_event_attributes,
425
+ )
426
+ session = self._new_session()
427
+ session.create()
428
+ else:
429
+ if not session.exists():
430
+ add_span_event(
431
+ current_span,
432
+ "Session is not valid, recreating it",
433
+ span_event_attributes,
434
+ )
435
+ session = self._new_session()
436
+ session.create()
437
+ return session
438
+
439
+ def put(self, session):
440
+ """Return a session to the pool.
441
+
442
+ Never blocks: if the pool is full, the returned session is
443
+ discarded.
444
+
445
+ :type session: :class:`~google.cloud.spanner_v1.session.Session`
446
+ :param session: the session being returned.
447
+ """
448
+ try:
449
+ self._sessions.put_nowait(session)
450
+ except queue.Full:
451
+ try:
452
+ # Sessions from pools are never multiplexed, so we can always delete them
453
+ session.delete()
454
+ except NotFound:
455
+ pass
456
+
457
+ def clear(self):
458
+ """Delete all sessions in the pool."""
459
+
460
+ while True:
461
+ try:
462
+ session = self._sessions.get(block=False)
463
+ except queue.Empty:
464
+ break
465
+ else:
466
+ session.delete()
467
+
468
+
469
+ class PingingPool(AbstractSessionPool):
470
+ """Concrete session pool implementation:
471
+
472
+ - Pre-allocates / creates a fixed number of sessions.
473
+
474
+ - Sessions are used in "round-robin" order (LRU first).
475
+
476
+ - "Pings" existing sessions in the background after a specified interval
477
+ via an API call (``session.ping()``).
478
+
479
+ - Blocks, with a timeout, when :meth:`get` is called on an empty pool.
480
+ Raises after timing out.
481
+
482
+ - Raises when :meth:`put` is called on a full pool. That error is
483
+ never expected in normal practice, as users should be calling
484
+ :meth:`get` followed by :meth:`put` whenever in need of a session.
485
+
486
+ The application is responsible for calling :meth:`ping` at appropriate
487
+ times, e.g. from a background thread.
488
+
489
+ :type size: int
490
+ :param size: fixed pool size
491
+
492
+ :type default_timeout: int
493
+ :param default_timeout: default timeout, in seconds, to wait for
494
+ a returned session.
495
+
496
+ :type ping_interval: int
497
+ :param ping_interval: interval at which to ping sessions.
498
+
499
+ :type labels: dict (str -> str) or None
500
+ :param labels: (Optional) user-assigned labels for sessions created
501
+ by the pool.
502
+
503
+ :type database_role: str
504
+ :param database_role: (Optional) user-assigned database_role for the session.
505
+ """
506
+
507
+ def __init__(
508
+ self,
509
+ size=10,
510
+ default_timeout=10,
511
+ ping_interval=3000,
512
+ labels=None,
513
+ database_role=None,
514
+ ):
515
+ super(PingingPool, self).__init__(labels=labels, database_role=database_role)
516
+ self.size = size
517
+ self.default_timeout = default_timeout
518
+ self._delta = datetime.timedelta(seconds=ping_interval)
519
+ self._sessions = queue.PriorityQueue(size)
520
+
521
+ def bind(self, database):
522
+ """Associate the pool with a database.
523
+
524
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
525
+ :param database: database used by the pool to create sessions
526
+ when needed.
527
+ """
528
+ self._database = database
529
+ api = database.spanner_api
530
+ metadata = _metadata_with_prefix(database.name)
531
+ if database._route_to_leader_enabled:
532
+ metadata.append(
533
+ _metadata_with_leader_aware_routing(database._route_to_leader_enabled)
534
+ )
535
+ self._database_role = self._database_role or self._database.database_role
536
+
537
+ request = BatchCreateSessionsRequest(
538
+ database=database.name,
539
+ session_count=self.size,
540
+ session_template=Session(creator_role=self.database_role),
541
+ )
542
+
543
+ span_event_attributes = {"kind": type(self).__name__}
544
+ current_span = get_current_span()
545
+ requested_session_count = request.session_count
546
+ if requested_session_count <= 0:
547
+ add_span_event(
548
+ current_span,
549
+ f"Invalid session pool size({requested_session_count}) <= 0",
550
+ span_event_attributes,
551
+ )
552
+ return
553
+
554
+ add_span_event(
555
+ current_span,
556
+ f"Requesting {requested_session_count} sessions",
557
+ span_event_attributes,
558
+ )
559
+
560
+ observability_options = getattr(self._database, "observability_options", None)
561
+ with trace_call(
562
+ "CloudSpanner.PingingPool.BatchCreateSessions",
563
+ observability_options=observability_options,
564
+ metadata=metadata,
565
+ ) as span, MetricsCapture():
566
+ returned_session_count = 0
567
+ while returned_session_count < self.size:
568
+ resp = api.batch_create_sessions(
569
+ request=request,
570
+ metadata=database.metadata_with_request_id(
571
+ database._next_nth_request,
572
+ 1,
573
+ metadata,
574
+ span,
575
+ ),
576
+ )
577
+
578
+ add_span_event(
579
+ span,
580
+ f"Created {len(resp.session)} sessions",
581
+ )
582
+
583
+ for session_pb in resp.session:
584
+ session = self._new_session()
585
+ returned_session_count += 1
586
+ session._session_id = session_pb.name.split("/")[-1]
587
+ self.put(session)
588
+
589
+ add_span_event(
590
+ span,
591
+ f"Requested for {requested_session_count} sessions, returned {returned_session_count}",
592
+ span_event_attributes,
593
+ )
594
+
595
+ def get(self, timeout=None):
596
+ """Check a session out from the pool.
597
+
598
+ :type timeout: int
599
+ :param timeout: seconds to block waiting for an available session
600
+
601
+ :rtype: :class:`~google.cloud.spanner_v1.session.Session`
602
+ :returns: an existing session from the pool, or a newly-created
603
+ session.
604
+ :raises: :exc:`queue.Empty` if the queue is empty.
605
+ """
606
+ if timeout is None:
607
+ timeout = self.default_timeout
608
+
609
+ start_time = time.time()
610
+ span_event_attributes = {"kind": type(self).__name__}
611
+ current_span = get_current_span()
612
+ add_span_event(
613
+ current_span,
614
+ "Waiting for a session to become available",
615
+ span_event_attributes,
616
+ )
617
+
618
+ ping_after = None
619
+ session = None
620
+ try:
621
+ ping_after, session = self._sessions.get(block=True, timeout=timeout)
622
+ except queue.Empty as e:
623
+ add_span_event(
624
+ current_span,
625
+ "No sessions available in the pool within the specified timeout",
626
+ span_event_attributes,
627
+ )
628
+ raise e
629
+
630
+ if _NOW() > ping_after:
631
+ # Using session.exists() guarantees the returned session exists.
632
+ # session.ping() uses a cached result in the backend which could
633
+ # result in a recently deleted session being returned.
634
+ if not session.exists():
635
+ session = self._new_session()
636
+ session.create()
637
+
638
+ span_event_attributes.update(
639
+ {
640
+ "time.elapsed": time.time() - start_time,
641
+ "session.id": session._session_id,
642
+ "kind": "pinging_pool",
643
+ }
644
+ )
645
+ add_span_event(current_span, "Acquired session", span_event_attributes)
646
+ return session
647
+
648
+ def put(self, session):
649
+ """Return a session to the pool.
650
+
651
+ Never blocks: if the pool is full, raises.
652
+
653
+ :type session: :class:`~google.cloud.spanner_v1.session.Session`
654
+ :param session: the session being returned.
655
+
656
+ :raises: :exc:`queue.Full` if the queue is full.
657
+ """
658
+ self._sessions.put_nowait((_NOW() + self._delta, session))
659
+
660
+ def clear(self):
661
+ """Delete all sessions in the pool."""
662
+ while True:
663
+ try:
664
+ _, session = self._sessions.get(block=False)
665
+ except queue.Empty:
666
+ break
667
+ else:
668
+ session.delete()
669
+
670
+ def ping(self):
671
+ """Refresh maybe-expired sessions in the pool.
672
+
673
+ This method is designed to be called from a background thread,
674
+ or during the "idle" phase of an event loop.
675
+ """
676
+ while True:
677
+ try:
678
+ ping_after, session = self._sessions.get(block=False)
679
+ except queue.Empty: # all sessions in use
680
+ break
681
+ if ping_after > _NOW(): # oldest session is fresh
682
+ # Re-add to queue with existing expiration
683
+ self._sessions.put((ping_after, session))
684
+ break
685
+ try:
686
+ session.ping()
687
+ except NotFound:
688
+ session = self._new_session()
689
+ session.create()
690
+ # Re-add to queue with new expiration
691
+ self.put(session)
692
+
693
+
694
+ class TransactionPingingPool(PingingPool):
695
+ """Concrete session pool implementation:
696
+
697
+ Deprecated: TransactionPingingPool no longer begins a transaction for each of its sessions at startup.
698
+ Hence the TransactionPingingPool is same as :class:`PingingPool` and maybe removed in the future.
699
+
700
+
701
+ In addition to the features of :class:`PingingPool`, this class
702
+ creates and begins a transaction for each of its sessions at startup.
703
+
704
+ When a session is returned to the pool, if its transaction has been
705
+ committed or rolled back, the pool creates a new transaction for the
706
+ session and pushes the transaction onto a separate queue of "transactions
707
+ to begin." The application is responsible for flushing this queue
708
+ as appropriate via the pool's :meth:`begin_pending_transactions` method.
709
+
710
+ :type size: int
711
+ :param size: fixed pool size
712
+
713
+ :type default_timeout: int
714
+ :param default_timeout: default timeout, in seconds, to wait for
715
+ a returned session.
716
+
717
+ :type ping_interval: int
718
+ :param ping_interval: interval at which to ping sessions.
719
+
720
+ :type labels: dict (str -> str) or None
721
+ :param labels: (Optional) user-assigned labels for sessions created
722
+ by the pool.
723
+
724
+ :type database_role: str
725
+ :param database_role: (Optional) user-assigned database_role for the session.
726
+ """
727
+
728
+ def __init__(
729
+ self,
730
+ size=10,
731
+ default_timeout=10,
732
+ ping_interval=3000,
733
+ labels=None,
734
+ database_role=None,
735
+ ):
736
+ """This throws a deprecation warning on initialization."""
737
+ warn(
738
+ f"{self.__class__.__name__} is deprecated.",
739
+ DeprecationWarning,
740
+ stacklevel=2,
741
+ )
742
+ self._pending_sessions = queue.Queue()
743
+
744
+ super(TransactionPingingPool, self).__init__(
745
+ size,
746
+ default_timeout,
747
+ ping_interval,
748
+ labels=labels,
749
+ database_role=database_role,
750
+ )
751
+
752
+ self.begin_pending_transactions()
753
+
754
+ def bind(self, database):
755
+ """Associate the pool with a database.
756
+
757
+ :type database: :class:`~google.cloud.spanner_v1.database.Database`
758
+ :param database: database used by the pool to create sessions
759
+ when needed.
760
+ """
761
+ super(TransactionPingingPool, self).bind(database)
762
+ self._database_role = self._database_role or self._database.database_role
763
+ self.begin_pending_transactions()
764
+
765
+ def put(self, session):
766
+ """Return a session to the pool.
767
+
768
+ Never blocks: if the pool is full, raises.
769
+
770
+ :type session: :class:`~google.cloud.spanner_v1.session.Session`
771
+ :param session: the session being returned.
772
+
773
+ :raises: :exc:`queue.Full` if the queue is full.
774
+ """
775
+ if self._sessions.full():
776
+ raise queue.Full
777
+
778
+ txn = session._transaction
779
+ if txn is None or txn.committed or txn.rolled_back:
780
+ session.transaction()
781
+ self._pending_sessions.put(session)
782
+ else:
783
+ super(TransactionPingingPool, self).put(session)
784
+
785
+ def begin_pending_transactions(self):
786
+ """Begin all transactions for sessions added to the pool."""
787
+ while not self._pending_sessions.empty():
788
+ session = self._pending_sessions.get()
789
+ super(TransactionPingingPool, self).put(session)
790
+
791
+
792
+ class SessionCheckout(object):
793
+ """Context manager: hold session checked out from a pool.
794
+
795
+ :type pool: concrete subclass of
796
+ :class:`~google.cloud.spanner_v1.pool.AbstractSessionPool`
797
+ :param pool: Pool from which to check out a session.
798
+
799
+ :param kwargs: extra keyword arguments to be passed to :meth:`pool.get`.
800
+ """
801
+
802
+ _session = None # Not checked out until '__enter__'.
803
+
804
+ def __init__(self, pool, **kwargs):
805
+ self._pool = pool
806
+ self._kwargs = kwargs.copy()
807
+
808
+ def __enter__(self):
809
+ self._session = self._pool.get(**self._kwargs)
810
+ return self._session
811
+
812
+ def __exit__(self, *ignored):
813
+ self._pool.put(self._session)