Skip to content

Commit

Permalink
Expand base client with a method for sending arbitrary commands. Will…
Browse files Browse the repository at this point in the history
… be used to implement pinterest#87.
  • Loading branch information
martinnj committed Apr 27, 2022
1 parent f77bc56 commit 78c1f0d
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 2 deletions.
70 changes: 68 additions & 2 deletions pymemcache/client/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,30 @@ def version(self):
raise MemcacheUnknownError("Received unexpected response: %s" % results[0])
return after

def misc(self, command, end_tokens="\r\n"):
"""
Sends an arbitrary command to the server and parses the response until a
specified token is encountered.
Args:
command: str|bytes: The command to send.
end_tokens: str|bytes: The token expected at the end of the
response. If the `end_token` is not found, the client will wait
until the timesout specified in the constructor.
Returns:
The response from the server, with the `end_token` removed.
"""
encoding = "utf8" if self.allow_unicode_keys else "ascii"
command = command.encode(encoding) if isinstance(command, str) else command
end_tokens = end_tokens.encode(encoding) if isinstance(end_tokens, str) else end_tokens
return self._misc_cmd(
[b"" + command + b"\r\n"],
command,
False,
end_tokens
)[0]

def flush_all(self, delay=0, noreply=None):
"""
The memcached "flush_all" command.
Expand Down Expand Up @@ -1126,7 +1150,14 @@ def _store_cmd(self, name, values, expire, noreply, flags=None, cas=None):
self.close()
raise

def _misc_cmd(self, cmds, cmd_name, noreply):
def _misc_cmd(self, cmds, cmd_name, noreply, end_tokens=None):

# If no end_tokens have been given, just assume standard memcached
# operations, which end in "\r\n", use regular code for that.
_reader = _readline
if end_tokens:
_reader = lambda _sock, _buf: _readsegment(_sock, _buf, end_tokens)

if self.sock is None:
self._connect()

Expand All @@ -1141,7 +1172,7 @@ def _misc_cmd(self, cmds, cmd_name, noreply):
line = None
for cmd in cmds:
try:
buf, line = _readline(self.sock, buf)
buf, line = _reader(self.sock, buf)
except MemcacheUnexpectedCloseError:
self.close()
raise
Expand Down Expand Up @@ -1502,6 +1533,41 @@ def _readvalue(sock, buf, size):
return buf[rlen:], b"".join(chunks)


def _readsegment(sock, buf, end_tokens):
"""Read a segment from the socket.
Read a segment from the socket, up to the first end_token sub-string/bytes,
and return that segment.
Args:
sock: Socket object, should be connected.
buf: bytes, zero or more bytes, returned from an earlier
call to _readline, _readsegment or _readvalue (pass an empty
byte-string on the first call).
end_tokens: bytes, indicates the end of the segment, generally this is
b"\\r\\n" for memcached.
Returns:
A tuple of (buf, line) where line is the full line read from the
socket (minus the end_tokens bytes) and buf is any trailing
characters read after the end_tokens was found (which may be an empty
bytes object).
"""
result = bytes()

while True:

if buf.find(end_tokens) != -1:
before, sep, after = buf.partition(end_tokens)
result += before
return after, result

buf = _recv(sock, RECV_SIZE)
if not buf:
raise MemcacheUnexpectedCloseError()


def _recv(sock, size):
"""sock.recv() with retry on EINTR"""
while True:
Expand Down
61 changes: 61 additions & 0 deletions pymemcache/test/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,67 @@ def test_version_exception(self):
with pytest.raises(MemcacheUnknownError):
client.version()

def test_misc_command_default_end_tokens(self):
client = self.make_client([b"REPLY\r\n", b"REPLY\r\nLEFTOVER"])
result = client.misc(b"misc")
assert result == b"REPLY"
result = client.misc(b"misc")
assert result == b"REPLY"

def test_misc_command_custom_end_tokens(self):
client = self.make_client([
b"REPLY\r\nEND\r\n",
b"REPLY\r\nEND\r\nLEFTOVER",
b"REPLYEND\r\nLEFTOVER",
b"REPLY\nLEFTOVER",
])
end_tokens = b"END\r\n"
result = client.misc(b"misc", end_tokens)
assert result == b"REPLY\r\n"
result = client.misc(b"misc", end_tokens)
assert result == b"REPLY\r\n"
result = client.misc(b"misc", end_tokens)
assert result == b"REPLY"
result = client.misc(b"misc", b"\n")
assert result == b"REPLY"

def test_misc_command_missing_end_tokens(self):
client = self.make_client([b"REPLY", b"REPLY"])
with pytest.raises(IndexError):
client.misc(b"misc")
with pytest.raises(IndexError):
client.misc(b"misc", b"END\r\n")

def test_misc_command_empty_end_tokens(self):
client = self.make_client([b"REPLY"])

with pytest.raises(IndexError):
client.misc(b"misc", b"")

def test_misc_command_types(self):
client = self.make_client([
b"REPLY\r\n",
b"REPLY\r\n",
b"REPLY\r\nLEFTOVER",
b"REPLY\r\nLEFTOVER"
])
assert client.misc("key") == b"REPLY"
assert client.misc(b"key") == b"REPLY"
assert client.misc("key") == b"REPLY"
assert client.misc(b"key") == b"REPLY"

def test_misc_end_token_types(self):
client = self.make_client([
b"REPLY\r\n",
b"REPLY\r\n",
b"REPLY\r\nLEFTOVER",
b"REPLY\r\nLEFTOVER"
])
assert client.misc("key", "\r\n") == b"REPLY"
assert client.misc(b"key", b"\r\n") == b"REPLY"
assert client.misc("key", "\r\n") == b"REPLY"
assert client.misc(b"key", b"\r\n") == b"REPLY"


@pytest.mark.unit()
class TestClientSocketConnect(unittest.TestCase):
Expand Down

0 comments on commit 78c1f0d

Please sign in to comment.