-
Notifications
You must be signed in to change notification settings - Fork 100
feat: Add support for multiplexed sessions - part 1 (supporting classes and refactoring) #1329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Add support for multiplexed sessions - part 1 (supporting classes and refactoring) #1329
Conversation
219c04e
to
99b9a75
Compare
if isinstance(exc_val, NotFound): | ||
# If NotFound exception occurs inside the with block | ||
# then we validate if the session still exists. | ||
if not self._session.exists(): | ||
self._session = self._database._pool._new_session() | ||
self._session.create() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check has been removed from MutationGroupsCheckout
and SnapshotCheckout
because:
exists
only checks whether the session exists on the server (not whether the local session object exists)- This checkout wasn't for all context managers (not in
BatchCheckout
orSessionCheckout
) - This seems to only be done as a slight optimization to potentially reduce overhead of session retrieval by adding it to the tail of a request instead of the head of another.
- In reality, the optimization only plays a part if the session expires while a call was happening, and is unlikely to make a big difference.
exists
checks are still performed by the session pool (for non-multiplexed session) and the database session manager (for multiplexed session).
We recommend removing this code, and instead depend on DatabaseSessionManager
to take care of checking session existence and re-creating them if necessary, rather than (sometimes) handling this in the context manager.
99b9a75
to
89b4d01
Compare
class SessionCheckout(object): | ||
"""Context manager for using a session from a database. | ||
|
||
:type database: :class:`~google.cloud.spanner_v1.database.Database` | ||
:param database: database to use the session from | ||
""" | ||
|
||
_session = None # Not checked out until '__enter__'. | ||
|
||
def __init__(self, database): | ||
if not isinstance(database, Database): | ||
raise TypeError( | ||
"{class_name} must receive an instance of {expected_class_name}. Received: {actual_class_name}".format( | ||
class_name=self.__class__.__name__, | ||
expected_class_name=Database.__name__, | ||
actual_class_name=database.__class__.__name__, | ||
) | ||
) | ||
|
||
self._database = database | ||
|
||
def __enter__(self): | ||
self._session = self._database._session_manager.get_session_for_read_write() | ||
return self._session | ||
|
||
def __exit__(self, *ignored): | ||
self._database._session_manager.put_session(self._session) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SessionCheckout
has been moved from pool.py
to database.py
, and updated so that the session is checked out from the database rather than from the pool.
Furthermore, SessionCheckout
's constructor previous accepted arbitrary keyword arguments, which were passed to the pool's get
method. While AbstractSessionPool.get
does not describe any keyword arguments, both FixedSizePool.get
and PingingPool.get
accept an optional timeout
keyword argument, which can be used to override the default timeout.
Because DatabaseSessionManager
now manages which session is returned, and it not guaranteed to return a session from the pool, we are suggesting removing the ability to provide arbitrary keyword arguments to the pool via SessionCheckout.__init__
.
No keyword arguments are supported by the default pool implementation (BurstyPool
), and users are already able to specify their own default timeout for FixedSizePool
and PingingPool
. Moreover, it is not clear to us where this functionality is even used: remaining uses of SessionCheckout
do not support these keyword arguments.
Please let us know:
- Is it acceptable to remove this functionality?
- Would you also like to remove support for the
timeout
arguments toFixedSizePool
andPingingPool
, for consistency?
if not isinstance(database, Database): | ||
raise TypeError( | ||
"{class_name} must receive an instance of {expected_class_name}. Received: {actual_class_name}".format( | ||
class_name=self.__class__.__name__, | ||
expected_class_name=Database.__name__, | ||
actual_class_name=database.__class__.__name__, | ||
) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better error handling, added a type check to all *Checkout
classes, along with corresponding unit tests.
database = Database( | ||
database_id=self.DATABASE_ID, instance=_Instance(self.INSTANCE_NAME) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of the added type check in the constructors for *Checkout
classes (see comment here), these tests have been updated to use a Database
instance instead of _Database
.
tests/unit/test_database.py
Outdated
def test_context_mgr_session_not_found_error(self): | ||
from google.cloud.exceptions import NotFound | ||
|
||
database = _Database(self.DATABASE_NAME) | ||
session = _Session(database, name="session-1") | ||
session.exists = mock.MagicMock(return_value=False) | ||
pool = database._pool = _Pool() | ||
new_session = _Session(database, name="session-2") | ||
new_session.create = mock.MagicMock(return_value=[]) | ||
pool._new_session = mock.MagicMock(return_value=new_session) | ||
session_manager.put_session.assert_called_once_with(session) | ||
|
||
pool.put(session) | ||
checkout = self._make_one(database) | ||
|
||
self.assertEqual(pool._session, session) | ||
with self.assertRaises(NotFound): | ||
with checkout as _: | ||
raise NotFound("Session not found") | ||
# Assert that session-1 was removed from pool and new session was added. | ||
self.assertEqual(pool._session, new_session) | ||
|
||
def test_context_mgr_table_not_found_error(self): | ||
from google.cloud.exceptions import NotFound | ||
|
||
database = _Database(self.DATABASE_NAME) | ||
session = _Session(database, name="session-1") | ||
session.exists = mock.MagicMock(return_value=True) | ||
pool = database._pool = _Pool() | ||
pool._new_session = mock.MagicMock(return_value=[]) | ||
|
||
pool.put(session) | ||
checkout = self._make_one(database) | ||
|
||
self.assertEqual(pool._session, session) | ||
with self.assertRaises(NotFound): | ||
with checkout as _: | ||
raise NotFound("Table not found") | ||
# Assert that session-1 was not removed from pool. | ||
self.assertEqual(pool._session, session) | ||
pool._new_session.assert_not_called() | ||
|
||
def test_context_mgr_unknown_error(self): | ||
database = _Database(self.DATABASE_NAME) | ||
session = _Session(database) | ||
pool = database._pool = _Pool() | ||
pool._new_session = mock.MagicMock(return_value=[]) | ||
pool.put(session) | ||
checkout = self._make_one(database) | ||
|
||
class Testing(Exception): | ||
pass | ||
|
||
self.assertEqual(pool._session, session) | ||
with self.assertRaises(Testing): | ||
with checkout as _: | ||
raise Testing("Unknown error.") | ||
# Assert that session-1 was not removed from pool. | ||
self.assertEqual(pool._session, session) | ||
pool._new_session.assert_not_called() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be 8000 displayed to describe this comment to others. Learn more.
These tests are no longer required because the context managers are no longer responsible for re-creating the session if it fails with a NotFound
error. See this comment for more details: https://github.com/googleapis/python-spanner/pull/1329/files#r2015092908
tests/unit/test_database.py
Outdated
def test_context_mgr_session_not_found_error(self): | ||
from google.cloud.exceptions import NotFound | ||
|
||
database = _Database(self.DATABASE_NAME) | ||
session = _Session(database, name="session-1") | ||
session.exists = mock.MagicMock(return_value=False) | ||
pool = database._pool = _Pool() | ||
new_session = _Session(database, name="session-2") | ||
new_session.create = mock.MagicMock(return_value=[]) | ||
pool._new_session = mock.MagicMock(return_value=new_session) | ||
class TestSessionCheckout(_BaseTest): | ||
def _get_target_class(self): | ||
from google.cloud.spanner_v1.database import SessionCheckout | ||
|
||
return SessionCheckout | ||
|
||
def test_ctor(self): | ||
from google.cloud.spanner_v1.database import Database | ||
|
||
database = Database( | ||
database_id=self.DATABASE_ID, instance=_Instance(self.INSTANCE_NAME) | ||
) | ||
|
||
pool.put(session) | ||
checkout = self._make_one(database) | ||
self.assertIs(checkout._database, database) | ||
self.assertIsNone(checkout._session) | ||
|
||
self.assertEqual(pool._session, session) | ||
with self.assertRaises(NotFound): | ||
with checkout as _: | ||
raise NotFound("Session not found") | ||
# Assert that session-1 was removed from pool and new session was added. | ||
self.assertEqual(pool._session, new_session) | ||
def test_context_manager_success(self): | ||
from google.cloud.spanner_v1.database import Database | ||
|
||
def test_context_mgr_table_not_found_error(self): | ||
from google.cloud.exceptions import NotFound | ||
database = Database( | ||
database_id=self.DATABASE_ID, instance=_Instance(self.INSTANCE_NAME) | ||
) | ||
|
||
database = _Database(self.DATABASE_NAME) | ||
session = _Session(database, name="session-1") | ||
session.exists = mock.MagicMock(return_value=True) | ||
pool = database._pool = _Pool() | ||
pool._new_session = mock.MagicMock(return_value=[]) | ||
session = _Session(database) | ||
session_manager = database._session_manager | ||
session_manager.get_session_for_read_write = mock.Mock(return_value=session) | ||
session_manager.put_session = mock.Mock(return_value=None) | ||
|
||
pool.put(session) | ||
checkout = self._make_one(database) | ||
|
||
self.assertEqual(pool._session, session) | ||
with self.assertRaises(NotFound): | ||
with checkout as _: | ||
raise NotFound("Table not found") | ||
# Assert that session-1 was not removed from pool. | ||
self.assertEqual(pool._session, session) | ||
pool._new_session.assert_not_called() | ||
|
||
def test_context_mgr_unknown_error(self): | ||
database = _Database(self.DATABASE_NAME) | ||
with checkout as borrowed: | ||
session_manager.get_session_for_read_write.assert_called_once() | ||
self.assertIs(borrowed, session) | ||
|
||
session_manager.put_session.assert_called_once_with(session) | ||
|
||
def test_context_manager_failure(self): | ||
from google.cloud.spanner_v1.database import Database | ||
|
||
database = Database( | ||
database_id=self.DATABASE_ID, instance=_Instance(self.INSTANCE_NAME) | ||
) | ||
|
||
session = _Session(database) | ||
pool = database._pool = _Pool() | ||
pool._new_session = mock.MagicMock(return_value=[]) | ||
pool.put(session) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests are no longer required because the context managers are no longer responsible for re-creating the session if it fails with a NotFound
error. See this comment for more details: https://github.com/googleapis/python-spanner/pull/1329/files#r2015092908
def test_ctor_w_kwargs(self): | ||
pool = _Pool() | ||
checkout = self._make_one(pool, foo="bar", database_role="dummy-role") | ||
self.assertIs(checkout._pool, pool) | ||
self.assertIsNone(checkout._session) | ||
self.assertEqual( | ||
checkout._kwargs, {"foo": "bar", "database_role": "dummy-role"} | ||
) | ||
|
||
def test_context_manager_wo_kwargs(self): | ||
session = object() | ||
pool = _Pool(session) | ||
checkout = self._make_one(pool) | ||
|
||
self.assertEqual(len(pool._items), 1) | ||
self.assertIs(pool._items[0], session) | ||
|
||
with checkout as borrowed: | ||
self.assertIs(borrowed, session) | ||
self.assertEqual(len(pool._items), 0) | ||
|
||
self.assertEqual(len(pool._items), 1) | ||
self.assertIs(pool._items[0], session) | ||
self.assertEqual(pool._got, {}) | ||
|
||
def test_context_manager_w_kwargs(self): | ||
session = object() | ||
pool = _Pool(session) | ||
checkout = self._make_one(pool, foo="bar") | ||
|
||
self.assertEqual(len(pool._items), 1) | ||
self.assertIs(pool._items[0], session) | ||
|
||
with checkout as borrowed: | ||
self.assertIs(borrowed, session) | ||
self.assertEqual(len(pool._items), 0) | ||
|
||
self.assertEqual(len(pool._items), 1) | ||
self.assertIs(pool._items[0], session) | ||
self.assertEqual(pool._got, {"foo": "bar"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These tests have mostly been moved to test_database.py
(since SessionCheckout
has moved to database.py
). The "w_kwargs" tests have been removed because they are no longer supported: see this comment here: https://github.com/googleapis/python-spanner/pull/1329/files#r2015125513
def _make_transaction(*args, **kw): | ||
from google.cloud.spanner_v1.transaction import Transaction | ||
|
||
txn = mock.create_autospec(Transaction)(*args, **kw) | ||
txn.committed = None | ||
txn.rolled_back = False | ||
return txn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method was unused so I just removed it.
def transaction(self): | ||
txn = self._transaction = _make_transaction(self) | ||
return txn | ||
|
||
@property | ||
def session_id(self): | ||
return self._session_id |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These were unused, so I just removed them.
0730ac1
to
97d8c41
Compare
f4238fc
to
58d588a
Compare
58d588a
to
3bb0647
Compare
…es and refactoring) - Adds SessionOptions and DatabaseSessionManager class support multiplexed session in future work. - Update Client, Instance, Database, Session and associated Checkout classes accordingly. - Add unit tests for new classes and update those for existing classes. Signed-off-by: currantw <taylor.curran@improving.com>
3bb0647
to
a8093f4
Compare
8619929
into
googleapis:multiplexed-sessions
…es and refactoring) (googleapis#1329) - Adds SessionOptions and DatabaseSessionManager class support multiplexed session in future work. - Update Client, Instance, Database, Session and associated Checkout classes accordingly. - Add unit tests for new classes and update those for existing classes. Signed-off-by: currantw <taylor.curran@improving.com>
Description
SessionOptions
andDatabaseSessionManager
class support multiplexed session in future work.Client
,Instance
,Database
,Session
and associatedCheckout
classes accordingly.