Repository: googleapis/google-cloud-python

Issue:
Spanner Unit Test b0rken on AppVeyor
https://ci.appveyor.com/project/GoogleCloudPlatform/google-cloud-python/build/1.0.1072.master/job/qvj0q69feleskbpu


Discussion:
@tseaver Having a not-green build is making me itchy. Are you planning on checking this out?
I will start on this issue today.  I have zero clue why the following would fail on Python 3.4 on Windows but not on Linux:

```python
____________________ TestTransactionPingingPool.test_bind _____________________
self = <unit_tests.test_pool.TestTransactionPingingPool testMethod=test_bind>
    def test_bind(self):
        pool = self._makeOne()
        database = _Database('name')
        SESSIONS = [_Session(database) for _ in range(10)]
        database._sessions.extend(SESSIONS)
    
>       pool.bind(database)
unit_tests\test_pool.py:567: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.tox\py34\lib\site-packages\google\cloud\spanner\pool.py:413: in bind
    self.begin_pending_transactions()
.tox\py34\lib\site-packages\google\cloud\spanner\pool.py:440: in begin_pending_transactions
    super(TransactionPingingPool, self).put(session)
.tox\py34\lib\site-packages\google\cloud\spanner\pool.py:340: in put
    self._sessions.put_nowait((_NOW() + self._delta, session))
c:\python34\Lib\queue.py:187: in put_nowait
    return self.put(item, block=False)
c:\python34\Lib\queue.py:146: in put
    self._put(item)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <queue.PriorityQueue object at 0x043BEF30>
item = (datetime.datetime(2017, 2, 15, 0, 14, 41, 439103), <unit_tests.test_pool._Session object at 0x043AED50>)
    def _put(self, item):
>       heappush(self.queue, item)
E       TypeError: unorderable types: _Session() < _Session()
c:\python34\Lib\queue.py:230: TypeError
```

(There is an identical failure in `unit_tests.test_pool.TestTransactionPingingPool testMethod=test_put_full`).
OK, my theory is that the clock resolution on the Appveyor Windows host is weird, such that successive calls to `datetime.datetime.utcnow()` are returning the same time, which makes the priority queue switch to the mock session to distinguish between entries.

I can work around the test failure by making `_Session` total-orderable:  likely I need to ensure that the "real" `Session` class can be ordered, too.

Resolving patch:
diff --git a/spanner/google/cloud/spanner/session.py b/spanner/google/cloud/spanner/session.py
--- a/spanner/google/cloud/spanner/session.py
+++ b/spanner/google/cloud/spanner/session.py
@@ -14,6 +14,7 @@
 
 """Wrapper for Cloud Spanner Session objects."""
 
+from functools import total_ordering
 import time
 
 from google.gax.errors import GaxError
@@ -34,6 +35,7 @@
 """Default timeout used by :meth:`Session.run_in_transaction`."""
 
 
+@total_ordering
 class Session(object):
     """Representation of a Cloud Spanner Session.
 
@@ -53,6 +55,9 @@ class Session(object):
     def __init__(self, database):
         self._database = database
 
+    def __lt__(self, other):
+        return self._session_id < other._session_id
+
     @property
     def session_id(self):
         """Read-only ID, set by the back-end during :meth:`create`."""


Test patch:
diff --git a/spanner/unit_tests/test_pool.py b/spanner/unit_tests/test_pool.py
--- a/spanner/unit_tests/test_pool.py
+++ b/spanner/unit_tests/test_pool.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 
 
+from functools import total_ordering
 import unittest
 
 
@@ -597,6 +598,32 @@ def test_bind(self):
 
         self.assertTrue(pool._pending_sessions.empty())
 
+    def test_bind_w_timestamp_race(self):
+        import datetime
+        from google.cloud._testing import _Monkey
+        from google.cloud.spanner import pool as MUT
+        NOW = datetime.datetime.utcnow()
+        pool = self._make_one()
+        database = _Database('name')
+        SESSIONS = [_Session(database) for _ in range(10)]
+        database._sessions.extend(SESSIONS)
+
+        with _Monkey(MUT, _NOW=lambda: NOW):
+            pool.bind(database)
+
+        self.assertIs(pool._database, database)
+        self.assertEqual(pool.size, 10)
+        self.assertEqual(pool.default_timeout, 10)
+        self.assertEqual(pool._delta.seconds, 3000)
+        self.assertTrue(pool._sessions.full())
+
+        for session in SESSIONS:
+            self.assertTrue(session._created)
+            txn = session._transaction
+            self.assertTrue(txn._begun)
+
+        self.assertTrue(pool._pending_sessions.empty())
+
     def test_put_full(self):
         from six.moves.queue import Full
 
@@ -755,6 +782,7 @@ def committed(self):
         return self._committed
 
 
+@total_ordering
 class _Session(object):
 
     _transaction = None
@@ -767,6 +795,9 @@ def __init__(self, database, exists=True, transaction=None):
         self._deleted = False
         self._transaction = transaction
 
+    def __lt__(self, other):
+        return id(self) < id(other)
+
     def create(self):
         self._created = True
 
diff --git a/spanner/unit_tests/test_session.py b/spanner/unit_tests/test_session.py
--- a/spanner/unit_tests/test_session.py
+++ b/spanner/unit_tests/test_session.py
@@ -42,6 +42,14 @@ def test_constructor(self):
         self.assertTrue(session.session_id is None)
         self.assertTrue(session._database is database)
 
+    def test___lt___(self):
+        database = _Database(self.DATABASE_NAME)
+        lhs = self._make_one(database)
+        lhs._session_id = b'123'
+        rhs = self._make_one(database)
+        rhs._session_id = b'234'
+        self.assertTrue(lhs < rhs)
+
     def test_name_property_wo_session_id(self):
         database = _Database(self.DATABASE_NAME)
         session = self._make_one(database)