Repository: aio-libs-abandoned/aioredis-py

Issue:
[2.0] Use loop.time() instead of time.time() for health checks
In the 2.0 alpha, health checks are based around `time.time`. Using `asyncio.get_event_loop().time()` has several advantages:
- The default implementation uses a monotonic clock, so it won't go wonky if the system time is changed.
- It sticks to a consistent source of time relative to things like `async_timeout`, which makes mocking out time easier (e.g. with [async-solipsism](https://github.com/bmerry/async-solipsism)).

I'm happy to submit a PR if there is agreement that this is a reasonable change.


Discussion:
I've created https://github.com/andymccurdy/redis-py/pull/1491 against redis-py to have it use monotonic time.
I believe we're trying to stick closely to redis-py implementation, so I'll +1 the ref PR to gain a bit more attention for discussion.

Resolving patch:
diff --git a/aioredis/client.py b/aioredis/client.py
--- a/aioredis/client.py
+++ b/aioredis/client.py
@@ -3,7 +3,6 @@
 import hashlib
 import inspect
 import re
-import time
 import time as mod_time
 import warnings
 from itertools import chain
@@ -4011,7 +4010,10 @@ async def check_health(self):
                 "did you forget to call subscribe() or psubscribe()?"
             )
 
-        if conn.health_check_interval and time.time() > conn.next_health_check:
+        if (
+            conn.health_check_interval
+            and asyncio.get_event_loop().time() > conn.next_health_check
+        ):
             await conn.send_command(
                 "PING", self.HEALTH_CHECK_MESSAGE, check_health=False
             )
diff --git a/aioredis/connection.py b/aioredis/connection.py
--- a/aioredis/connection.py
+++ b/aioredis/connection.py
@@ -6,7 +6,6 @@
 import socket
 import ssl
 import threading
-import time
 import warnings
 from distutils.version import StrictVersion
 from itertools import chain
@@ -600,7 +599,7 @@ def __init__(
         self.socket_type = socket_type
         self.retry_on_timeout = retry_on_timeout
         self.health_check_interval = health_check_interval
-        self.next_health_check = 0
+        self.next_health_check = -1
         self.ssl_context: Optional[RedisSSLContext] = None
         self.encoder = encoder_class(encoding, encoding_errors, decode_responses)
         self._reader: Optional[asyncio.StreamReader] = None
@@ -772,7 +771,10 @@ async def disconnect(self):
 
     async def check_health(self):
         """Check the health of the connection with a PING/PONG"""
-        if self.health_check_interval and time.time() > self.next_health_check:
+        if (
+            self.health_check_interval
+            and asyncio.get_event_loop().time() > self.next_health_check
+        ):
             try:
                 await self.send_command("PING", check_health=False)
                 if str_if_bytes(await self.read_response()) != "PONG":
@@ -858,7 +860,9 @@ async def read_response(self):
             raise
 
         if self.health_check_interval:
-            self.next_health_check = time.time() + self.health_check_interval
+            self.next_health_check = (
+                asyncio.get_event_loop().time() + self.health_check_interval
+            )
 
         if isinstance(response, ResponseError):
             raise response from None
@@ -1056,7 +1060,7 @@ def __init__(
         self.socket_timeout = socket_timeout
         self.retry_on_timeout = retry_on_timeout
         self.health_check_interval = health_check_interval
-        self.next_health_check = 0
+        self.next_health_check = -1
         self.encoder = Encoder(encoding, encoding_errors, decode_responses)
         self._sock = None
         self._reader = None
diff --git a/aioredis/lock.py b/aioredis/lock.py
--- a/aioredis/lock.py
+++ b/aioredis/lock.py
@@ -1,6 +1,5 @@
 import asyncio
 import threading
-import time as mod_time
 import uuid
 from types import SimpleNamespace
 from typing import TYPE_CHECKING, Awaitable, NoReturn, Union
@@ -186,6 +185,7 @@ async def acquire(
         object with the default encoding. If a token isn't specified, a UUID
         will be generated.
         """
+        loop = asyncio.get_event_loop()
         sleep = self.sleep
         if token is None:
             token = uuid.uuid1().hex.encode()
@@ -198,14 +198,14 @@ async def acquire(
             blocking_timeout = self.blocking_timeout
         stop_trying_at = None
         if blocking_timeout is not None:
-            stop_trying_at = mod_time.monotonic() + blocking_timeout
+            stop_trying_at = loop.time() + blocking_timeout
         while True:
             if await self.do_acquire(token):
                 self.local.token = token
                 return True
             if not blocking:
                 return False
-            next_try_at = mod_time.monotonic() + sleep
+            next_try_at = loop.time() + sleep
             if stop_trying_at is not None and next_try_at > stop_trying_at:
                 return False
             await asyncio.sleep(sleep)


Test patch:
diff --git a/tests/test_connection_pool.py b/tests/test_connection_pool.py
--- a/tests/test_connection_pool.py
+++ b/tests/test_connection_pool.py
@@ -1,7 +1,6 @@
 import asyncio
 import os
 import re
-import time
 
 import pytest
 
@@ -138,11 +137,11 @@ async def test_connection_pool_blocks_until_timeout(self, master_host):
         )
         await pool.get_connection("_")
 
-        start = time.time()
+        start = asyncio.get_event_loop().time()
         with pytest.raises(aioredis.ConnectionError):
             await pool.get_connection("_")
         # we should have waited at least 0.1 seconds
-        assert time.time() - start >= 0.1
+        assert asyncio.get_event_loop().time() - start >= 0.1
 
     async def test_connection_pool_blocks_until_conn_available(self, master_host):
         """
@@ -159,9 +158,9 @@ async def target():
             await asyncio.sleep(0.1)
             await pool.release(c1)
 
-        start = time.time()
+        start = asyncio.get_event_loop().time()
         await asyncio.gather(target(), pool.get_connection("_"))
-        assert time.time() - start >= 0.1
+        assert asyncio.get_event_loop().time() - start >= 0.1
 
     async def test_reuse_previously_released_connection(self, master_host):
         connection_kwargs = {"host": master_host}
@@ -595,12 +594,12 @@ async def r(self, create_redis):
         await redis.flushall()
 
     def assert_interval_advanced(self, connection):
-        diff = connection.next_health_check - time.time()
+        diff = connection.next_health_check - asyncio.get_event_loop().time()
         assert self.interval > diff > (self.interval - 1)
 
     async def test_health_check_runs(self, r):
         if r.connection:
-            r.connection.next_health_check = time.time() - 1
+            r.connection.next_health_check = asyncio.get_event_loop().time() - 1
             await r.connection.check_health()
             self.assert_interval_advanced(r.connection)
 
@@ -608,7 +607,7 @@ async def test_arbitrary_command_invokes_health_check(self, r):
         # invoke a command to make sure the connection is entirely setup
         if r.connection:
             await r.get("foo")
-            r.connection.next_health_check = time.time()
+            r.connection.next_health_check = asyncio.get_event_loop().time()
             with mock.patch.object(
                 r.connection, "send_command", wraps=r.connection.send_command
             ) as m:
diff --git a/tests/test_lock.py b/tests/test_lock.py
--- a/tests/test_lock.py
+++ b/tests/test_lock.py
@@ -1,4 +1,4 @@
-import time
+import asyncio
 
 import pytest
 
@@ -104,10 +104,10 @@ async def test_blocking_timeout(self, r):
         bt = 0.2
         sleep = 0.05
         lock2 = self.get_lock(r, "foo", sleep=sleep, blocking_timeout=bt)
-        start = time.monotonic()
+        start = asyncio.get_event_loop().time()
         assert not await lock2.acquire()
         # The elapsed duration should be less than the total blocking_timeout
-        assert bt > (time.monotonic() - start) > bt - sleep
+        assert bt > (asyncio.get_event_loop().time() - start) > bt - sleep
         await lock1.release()
 
     async def test_context_manager(self, r):
@@ -129,11 +129,11 @@ async def test_high_sleep_small_blocking_timeout(self, r):
         sleep = 60
         bt = 1
         lock2 = self.get_lock(r, "foo", sleep=sleep, blocking_timeout=bt)
-        start = time.monotonic()
+        start = asyncio.get_event_loop().time()
         assert not await lock2.acquire()
         # the elapsed timed is less than the blocking_timeout as the lock is
         # unattainable given the sleep/blocking_timeout configuration
-        assert bt > (time.monotonic() - start)
+        assert bt > (asyncio.get_event_loop().time() - start)
         await lock1.release()
 
     async def test_releasing_unlocked_lock_raises_error(self, r):
diff --git a/tests/test_pubsub.py b/tests/test_pubsub.py
--- a/tests/test_pubsub.py
+++ b/tests/test_pubsub.py
@@ -14,7 +14,7 @@
 
 
 async def wait_for_message(pubsub, timeout=0.1, ignore_subscribe_messages=False):
-    now = time.time()
+    now = asyncio.get_event_loop().time()
     timeout = now + timeout
     while now < timeout:
         message = await pubsub.get_message(
@@ -23,7 +23,7 @@ async def wait_for_message(pubsub, timeout=0.1, ignore_subscribe_messages=False)
         if message is not None:
             return message
         await asyncio.sleep(0.01)
-        now = time.time()
+        now = asyncio.get_event_loop().time()
     return None