Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions pymemcache/client/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,10 @@ def _retry_dead(self) -> None:
current_time = time.time()
ldc = self._last_dead_check_time
# We have reached the retry timeout
if current_time - ldc > self.dead_timeout:
if current_time - ldc >= self.dead_timeout:
candidates = []
for server, dead_time in self._dead_clients.items():
if current_time - dead_time > self.dead_timeout:
if current_time - dead_time >= self.dead_timeout:
candidates.append(server)
for server in candidates:
logger.debug("bringing server back into rotation %s", server)
Expand Down Expand Up @@ -200,7 +200,7 @@ def _safely_run_func(self, client, func, default_val, *args, **kwargs):
# time lets just retry using it
if failed_metadata["attempts"] < self.retry_attempts:
failed_time = failed_metadata["failed_time"]
if time.time() - failed_time > self.retry_timeout:
if time.time() - failed_time >= self.retry_timeout:
logger.debug("retrying failed server: %s", client.server)
result = func(*args, **kwargs)
# we were successful, lets remove it from the failed
Expand Down Expand Up @@ -249,7 +249,7 @@ def _safely_run_set_many(self, client, values, *args, **kwargs):
# time lets just retry using it
if failed_metadata["attempts"] < self.retry_attempts:
failed_time = failed_metadata["failed_time"]
if time.time() - failed_time > self.retry_timeout:
if time.time() - failed_time >= self.retry_timeout:
logger.debug("retrying failed server: %s", client.server)
succeeded, failed, err = self._set_many(
client, values, *args, **kwargs
Expand Down
33 changes: 33 additions & 0 deletions pymemcache/test/test_client_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,7 @@ def test_server_encoding_client(self):
for client in hash_client.clients.values():
assert client.encoding == encoding

@mock.patch("pymemcache.client.hash.time.time", new=lambda: 100.0)
@mock.patch("pymemcache.client.hash.HashClient.client_class")
def test_dead_server_comes_back(self, client_patch):
client = HashClient([], dead_timeout=0, retry_attempts=0)
Expand All @@ -482,6 +483,7 @@ def test_dead_server_comes_back(self, client_patch):
assert client.get(b"key") == "Some value"
assert ("127.0.0.1", 11211) not in client._dead_clients

@mock.patch("pymemcache.client.hash.time.time", new=lambda: 100.0)
@mock.patch("pymemcache.client.hash.HashClient.client_class")
def test_failed_is_retried(self, client_patch):
client = HashClient([], retry_attempts=1, retry_timeout=0)
Expand Down Expand Up @@ -561,3 +563,34 @@ def test_legacy_add_remove_server_signature(self):
client.remove_server(server, server[-1])

# TODO: Test failover logic


@pytest.mark.unit
@pytest.mark.parametrize("timeout", [0, 5])
@pytest.mark.parametrize(
"command,argument,result",
[("get", b"key", b"value"), ("set_many", {b"key": b"value"}, [])],
)
def test_retry_at_timeout_boundary(timeout, command, argument, result):
with mock.patch("pymemcache.client.hash.time.time", return_value=100) as clock:
with mock.patch(
"pymemcache.client.hash.HashClient.client_class"
) as client_class:
client = HashClient([("localhost", 11211)], retry_timeout=timeout)
inner = client_class.return_value
inner.server = ("localhost", 11211)
method = getattr(inner, command)
method.side_effect = socket.timeout()
with pytest.raises(socket.timeout):
getattr(client, command)(argument)

method.side_effect = None
method.return_value = result
if timeout:
clock.return_value = 100 + timeout - 1
getattr(client, command)(argument)
assert method.call_count == 1

clock.return_value = 100 + timeout
assert getattr(client, command)(argument) == result
assert method.call_count == 2