feat(memcache): Add support for GETS (#5087)

Support is added for the GETS command. A placeholder CAS token of 0 is
always returned.

Signed-off-by: Abhijat Malviya <abhijat@dragonflydb.io>
This commit is contained in:
Abhijat Malviya 2025-05-09 13:31:07 +05:30 committed by GitHub
parent fe495cde3f
commit fc00f2c972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 34 additions and 11 deletions

View file

@ -212,7 +212,7 @@ MCReplyBuilder::MCReplyBuilder(::io::Sink* sink) : SinkReplyBuilder(sink), all_(
} }
void MCReplyBuilder::SendValue(std::string_view key, std::string_view value, uint64_t mc_ver, void MCReplyBuilder::SendValue(std::string_view key, std::string_view value, uint64_t mc_ver,
uint32_t mc_flag) { uint32_t mc_flag, bool send_cas_token) {
ReplyScope scope(this); ReplyScope scope(this);
if (flag_.meta) { if (flag_.meta) {
string flags; string flags;
@ -227,7 +227,7 @@ void MCReplyBuilder::SendValue(std::string_view key, std::string_view value, uin
} }
} else { } else {
WritePieces("VALUE ", key, " ", mc_flag, " ", value.size()); WritePieces("VALUE ", key, " ", mc_flag, " ", value.size());
if (mc_ver) if (send_cas_token)
WritePieces(" ", mc_ver); WritePieces(" ", mc_ver);
if (value.size() <= kMaxInlineSize) { if (value.size() <= kMaxInlineSize) {

View file

@ -177,7 +177,8 @@ class MCReplyBuilder : public SinkReplyBuilder {
void SendDeleted(); void SendDeleted();
void SendGetEnd(); void SendGetEnd();
void SendValue(std::string_view key, std::string_view value, uint64_t mc_ver, uint32_t mc_flag); void SendValue(std::string_view key, std::string_view value, uint64_t mc_ver, uint32_t mc_flag,
bool send_cas_token);
void SendSimpleString(std::string_view str) final; void SendSimpleString(std::string_view str) final;
void SendProtocolError(std::string_view str) final; void SendProtocolError(std::string_view str) final;

View file

@ -341,6 +341,9 @@ TEST_F(DflyEngineTest, Memcache) {
auto resp = RunMC(MP::SET, "key", "bar", 1); auto resp = RunMC(MP::SET, "key", "bar", 1);
EXPECT_THAT(resp, ElementsAre("STORED")); EXPECT_THAT(resp, ElementsAre("STORED"));
resp = RunMC(MP::GETS, "key");
EXPECT_THAT(resp, ElementsAre("VALUE key 1 3 0", "bar", "END"));
resp = RunMC(MP::GET, "key"); resp = RunMC(MP::GET, "key");
EXPECT_THAT(resp, ElementsAre("VALUE key 1 3", "bar", "END")); EXPECT_THAT(resp, ElementsAre("VALUE key 1 3", "bar", "END"));
@ -366,6 +369,9 @@ TEST_F(DflyEngineTest, Memcache) {
resp = RunMC(MP::GET, "unkn"); resp = RunMC(MP::GET, "unkn");
EXPECT_THAT(resp, ElementsAre("END")); EXPECT_THAT(resp, ElementsAre("END"));
resp = GetMC(MP::GETS, {"key", "key2", "unknown"});
EXPECT_THAT(resp, ElementsAre("VALUE key 1 3 0", "bar", "VALUE key2 2 8 0", "bar2val2", "END"));
} }
TEST_F(DflyEngineTest, MemcacheFlags) { TEST_F(DflyEngineTest, MemcacheFlags) {

View file

@ -1536,6 +1536,8 @@ void Service::DispatchMC(const MemcacheParser::Command& cmd, std::string_view va
strcpy(cmd_name, "PREPEND"); strcpy(cmd_name, "PREPEND");
break; break;
case MemcacheParser::GET: case MemcacheParser::GET:
[[fallthrough]];
case MemcacheParser::GETS:
strcpy(cmd_name, "MGET"); strcpy(cmd_name, "MGET");
break; break;
case MemcacheParser::FLUSHALL: case MemcacheParser::FLUSHALL:
@ -1589,6 +1591,9 @@ void Service::DispatchMC(const MemcacheParser::Command& cmd, std::string_view va
char* key = const_cast<char*>(s.data()); char* key = const_cast<char*>(s.data());
args.emplace_back(key, s.size()); args.emplace_back(key, s.size());
} }
if (cmd.type == MemcacheParser::GETS) {
dfly_cntx->conn_state.memcache_flag |= ConnectionState::FETCH_CAS_VER;
}
} else { // write commands. } else { // write commands.
if (store_opt[0]) { if (store_opt[0]) {
args.emplace_back(store_opt, strlen(store_opt)); args.emplace_back(store_opt, strlen(store_opt));

View file

@ -1366,7 +1366,7 @@ void StringFamily::MGet(CmdArgList args, const CommandContext& cmnd_cntx) {
DCHECK(dynamic_cast<CapturingReplyBuilder*>(builder) == nullptr); DCHECK(dynamic_cast<CapturingReplyBuilder*>(builder) == nullptr);
for (const auto& entry : res) { for (const auto& entry : res) {
if (entry) { if (entry) {
rb->SendValue(entry->key, entry->value, entry->mc_ver, entry->mc_flag); rb->SendValue(entry->key, entry->value, 0, entry->mc_flag, fetch_mask & FETCH_MCVER);
} else { } else {
rb->SendMiss(); rb->SendMiss();
} }

View file

@ -6,7 +6,7 @@ from meta_memcache import (
CacheClient, CacheClient,
connection_pool_factory_builder, connection_pool_factory_builder,
) )
from meta_memcache.protocol import RequestFlags, Miss, Value, Success from meta_memcache.protocol import RequestFlags, Success
DEFAULT_ARGS = {"memcached_port": 11211, "proactor_threads": 4} DEFAULT_ARGS = {"memcached_port": 11211, "proactor_threads": 4}
@ -30,3 +30,12 @@ def test_basic(df_server: DflyInstance):
assert pool.get("key2") is None assert pool.get("key2") is None
assert pool.delete("key1") assert pool.delete("key1")
assert pool.delete("key1") is False assert pool.delete("key1") is False
assert pool.set("cask", "v", 100)
value, cas_token = pool.get_cas("cask")
assert value == "v" and cas_token == 0
k = Key("cask")
response = pool.meta_multiget([k], RequestFlags(return_cas_token=True, return_value=True))
assert k in response
assert response[k].flags.cas_token == 0 and response[k].value == "v"

View file

@ -1,17 +1,17 @@
import logging import logging
import pytest
from pymemcache.client.base import Client as MCClient
from redis import Redis
import socket
import random import random
import time import socket
import ssl import ssl
import time
from pymemcache.client.base import Client as MCClient
from . import dfly_args from . import dfly_args
from .instance import DflyInstance from .instance import DflyInstance
DEFAULT_ARGS = {"memcached_port": 11211, "proactor_threads": 4} DEFAULT_ARGS = {"memcached_port": 11211, "proactor_threads": 4}
# Generic basic tests # Generic basic tests
@ -32,7 +32,7 @@ def test_basic(memcached_client: MCClient):
# delete # delete
assert memcached_client.delete("key1") assert memcached_client.delete("key1")
assert not memcached_client.delete("key3") assert not memcached_client.delete("key3")
assert memcached_client.get("key1") == None assert memcached_client.get("key1") is None
# prepend append # prepend append
assert memcached_client.set("key4", "B") assert memcached_client.set("key4", "B")
@ -46,6 +46,8 @@ def test_basic(memcached_client: MCClient):
assert memcached_client.incr("key5", 1) == 2 assert memcached_client.incr("key5", 1) == 2
assert memcached_client.decr("key5", 1) == 1 assert memcached_client.decr("key5", 1) == 1
assert memcached_client.gets("key5") == (b"1", b"0")
# Noreply (and pipeline) tests # Noreply (and pipeline) tests