This is an automated email from the ASF dual-hosted git repository.

jihuayu pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 27eae0d19 feat(commands): implement CLIENT SETINFO subcommand (#3465)
27eae0d19 is described below

commit 27eae0d199b6a40b296d895073658354850087d6
Author: gongna-au <[email protected]>
AuthorDate: Tue Apr 28 17:47:13 2026 +0800

    feat(commands): implement CLIENT SETINFO subcommand (#3465)
    
    ## Summary
    
    Implement the `CLIENT SETINFO` subcommand (introduced in Redis 7.2) to
    allow clients to set `lib-name` and `lib-ver` metadata on the
    connection. Many modern Redis SDKs (go-redis v9, jedis 5, redis-py 5)
    automatically send `CLIENT SETINFO` on connect — without this support,
    those clients log errors on every new connection.
    
    ## Changes
    
    - **`src/server/redis_connection.h`**: Add `lib_name_` / `lib_ver_`
    fields and getters/setters to the `Connection` class.
    - **`src/server/redis_connection.cc`**: Include `lib-name=` and
    `lib-ver=` in `CLIENT LIST` / `CLIENT INFO` output, matching the Redis
    output format.
    - **`src/commands/cmd_server.cc`**: Add `SETINFO` subcommand parsing and
    execution in `CommandClient`. Supports `LIB-NAME` and `LIB-VER`
    attributes, validates charset (same rules as `SETNAME`), and rejects
    unrecognized attributes.
    - **`tests/gocase/unit/client/client_setinfo_test.go`**: Integration
    tests covering set/get, `CLIENT INFO`/`LIST` output, case-insensitivity,
    empty value (clear), unknown attribute rejection, and invalid
    characters.
    
    ## Behavior
    
    ```
    > CLIENT SETINFO LIB-NAME go-redis
    OK
    > CLIENT SETINFO LIB-VER 9.7.0
    OK
    > CLIENT INFO
    id=3 addr=127.0.0.1:6379 fd=8 name= age=5 idle=0 flags=N namespace= qbuf=0 
obuf=0 cmd=client lib-name=go-redis lib-ver=9.7.0
    ```
    
    - `CLIENT SETINFO LIB-NAME <value>` — sets the client library name
    - `CLIENT SETINFO LIB-VER <value>` — sets the client library version
    - Empty string clears the field
    - Attribute names are case-insensitive
    - Values follow the same charset restriction as `CLIENT SETNAME` (no
    spaces/control chars)
    - Unrecognized attributes return: `ERR Unrecognized option '<attr>'`
    
    ---------
    
    Co-authored-by: gongna-au <[email protected]>
    Co-authored-by: 纪华裕 <[email protected]>
---
 src/commands/cmd_server.cc                      |  40 ++++++++-
 src/server/redis_connection.cc                  |   7 +-
 src/server/redis_connection.h                   |   9 ++
 tests/gocase/unit/client/client_setinfo_test.go | 105 ++++++++++++++++++++++++
 tests/gocase/unit/monitor/monitor_test.go       |   5 +-
 5 files changed, 159 insertions(+), 7 deletions(-)

diff --git a/src/commands/cmd_server.cc b/src/commands/cmd_server.cc
index 35a85c442..a624756db 100644
--- a/src/commands/cmd_server.cc
+++ b/src/commands/cmd_server.cc
@@ -504,6 +504,31 @@ class CommandClient : public Commander {
       return Status::OK();
     }
 
+    if (subcommand_ == "setinfo") {
+      if (args.size() != 4) {
+        return {Status::RedisParseErr, errInvalidSyntax};
+      }
+
+      auto attr = util::ToLower(args[2]);
+
+      for (auto ch : args[3]) {
+        if (ch < '!' || ch > '~') {
+          return {Status::RedisInvalidCmd,
+                  "lib-name and lib-ver cannot contain spaces, newlines or 
special characters"};
+        }
+      }
+
+      if (attr == "lib-name") {
+        setinfo_lib_name_ = args[3];
+      } else if (attr == "lib-ver") {
+        setinfo_lib_ver_ = args[3];
+      } else {
+        return {Status::RedisInvalidCmd, "Unrecognized option '" + args[2] + 
"'"};
+      }
+
+      return Status::OK();
+    }
+
     if (subcommand_ == "reply") {
       if (args.size() != 3) {
         return {Status::RedisParseErr, errInvalidSyntax};
@@ -602,7 +627,7 @@ class CommandClient : public Commander {
       return Status::OK();
     }
     return {Status::RedisInvalidCmd,
-            "Syntax error, try CLIENT LIST|INFO|KILL 
ip:port|GETNAME|SETNAME|REPLY|"
+            "Syntax error, try CLIENT LIST|INFO|KILL 
ip:port|GETNAME|SETNAME|SETINFO|REPLY|"
             "PAUSE|UNPAUSE"};
   }
 
@@ -659,10 +684,19 @@ class CommandClient : public Commander {
       srv->UnpauseConns();
       *output = redis::RESP_OK;
       return Status::OK();
+    } else if (subcommand_ == "setinfo") {
+      if (setinfo_lib_name_) {
+        conn->SetLibName(*setinfo_lib_name_);
+      }
+      if (setinfo_lib_ver_) {
+        conn->SetLibVer(*setinfo_lib_ver_);
+      }
+      *output = redis::RESP_OK;
+      return Status::OK();
     }
 
     return {Status::RedisInvalidCmd,
-            "Syntax error, try CLIENT LIST|INFO|KILL 
ip:port|GETNAME|SETNAME|REPLY|"
+            "Syntax error, try CLIENT LIST|INFO|KILL 
ip:port|GETNAME|SETNAME|SETINFO|REPLY|"
             "PAUSE|UNPAUSE"};
   }
 
@@ -670,6 +704,8 @@ class CommandClient : public Commander {
   std::string addr_;
   std::string conn_name_;
   std::string subcommand_;
+  std::optional<std::string> setinfo_lib_name_;
+  std::optional<std::string> setinfo_lib_ver_;
   redis::Connection::ReplyMode reply_mode_ = redis::Connection::ReplyMode::ON;
   bool skipme_ = false;
   int64_t kill_type_ = 0;
diff --git a/src/server/redis_connection.cc b/src/server/redis_connection.cc
index f9db92b0c..aa301b32f 100644
--- a/src/server/redis_connection.cc
+++ b/src/server/redis_connection.cc
@@ -87,9 +87,10 @@ std::string Connection::ToString() {
     db_or_ns_value = ns_;
   }
 
-  return fmt::format("id={} addr={} fd={} name={} age={} idle={} flags={} 
{}={} qbuf={} obuf={} cmd={}\n", id_, addr_,
-                     bufferevent_getfd(bev_), name_, GetAge(), GetIdleTime(), 
GetFlags(), db_or_ns_field,
-                     db_or_ns_value, evbuffer_get_length(Input()), 
evbuffer_get_length(Output()), last_cmd_);
+  return fmt::format(
+      "id={} addr={} fd={} name={} age={} idle={} flags={} {}={} qbuf={} 
obuf={} cmd={} lib-name={} lib-ver={}\n", id_,
+      addr_, bufferevent_getfd(bev_), name_, GetAge(), GetIdleTime(), 
GetFlags(), db_or_ns_field, db_or_ns_value,
+      evbuffer_get_length(Input()), evbuffer_get_length(Output()), last_cmd_, 
set_info_.lib_name, set_info_.lib_ver);
 }
 
 void Connection::Close() {
diff --git a/src/server/redis_connection.h b/src/server/redis_connection.h
index 4eba076e6..909ce8759 100644
--- a/src/server/redis_connection.h
+++ b/src/server/redis_connection.h
@@ -137,6 +137,14 @@ class Connection : public EvbufCallbackBase<Connection> {
   void SetID(uint64_t id) { id_ = id; }
   std::string GetName() const { return name_; }
   void SetName(std::string name) { name_ = std::move(name); }
+  struct SetInfo {
+    std::string lib_name;
+    std::string lib_ver;
+  };
+
+  const SetInfo &GetSetInfo() const { return set_info_; }
+  void SetLibName(std::string lib_name) { set_info_.lib_name = 
std::move(lib_name); }
+  void SetLibVer(std::string lib_ver) { set_info_.lib_ver = 
std::move(lib_ver); }
   std::string GetAddr() const { return addr_; }
   void SetAddr(std::string ip, uint32_t port);
   void SetLastCmd(std::string cmd) { last_cmd_ = std::move(cmd); }
@@ -203,6 +211,7 @@ class Connection : public EvbufCallbackBase<Connection> {
   std::atomic<int> flags_ = 0;
   std::string ns_;
   std::string name_;
+  SetInfo set_info_;
   std::string ip_;
   std::string announce_ip_;
   uint32_t port_ = 0;
diff --git a/tests/gocase/unit/client/client_setinfo_test.go 
b/tests/gocase/unit/client/client_setinfo_test.go
new file mode 100644
index 000000000..356626731
--- /dev/null
+++ b/tests/gocase/unit/client/client_setinfo_test.go
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package client
+
+import (
+       "context"
+       "strings"
+       "testing"
+
+       "github.com/apache/kvrocks/tests/gocase/util"
+       "github.com/stretchr/testify/require"
+)
+
+func TestClientSetInfo(t *testing.T) {
+       srv := util.StartServer(t, map[string]string{})
+       defer srv.Close()
+
+       ctx := context.Background()
+       rdb := srv.NewClient()
+       defer func() { require.NoError(t, rdb.Close()) }()
+
+       t.Run("CLIENT SETINFO LIB-NAME sets library name", func(t *testing.T) {
+               res, err := rdb.Do(ctx, "CLIENT", "SETINFO", "LIB-NAME", 
"my-lib").Result()
+               require.NoError(t, err)
+               require.Equal(t, "OK", res)
+       })
+
+       t.Run("CLIENT SETINFO LIB-VER sets library version", func(t *testing.T) 
{
+               res, err := rdb.Do(ctx, "CLIENT", "SETINFO", "LIB-VER", 
"1.2.3").Result()
+               require.NoError(t, err)
+               require.Equal(t, "OK", res)
+       })
+
+       t.Run("CLIENT INFO shows lib-name and lib-ver", func(t *testing.T) {
+               info, err := rdb.Do(ctx, "CLIENT", "INFO").Result()
+               require.NoError(t, err)
+               infoStr, ok := info.(string)
+               require.True(t, ok)
+               require.Contains(t, infoStr, "lib-name=my-lib")
+               require.Contains(t, infoStr, "lib-ver=1.2.3")
+       })
+
+       t.Run("CLIENT LIST shows lib-name and lib-ver", func(t *testing.T) {
+               list, err := rdb.Do(ctx, "CLIENT", "LIST").Result()
+               require.NoError(t, err)
+               listStr, ok := list.(string)
+               require.True(t, ok)
+               require.Contains(t, listStr, "lib-name=my-lib")
+               require.Contains(t, listStr, "lib-ver=1.2.3")
+       })
+
+       t.Run("CLIENT SETINFO is case-insensitive for attribute name", func(t 
*testing.T) {
+               res, err := rdb.Do(ctx, "CLIENT", "SETINFO", "lib-name", 
"lower-lib").Result()
+               require.NoError(t, err)
+               require.Equal(t, "OK", res)
+
+               info, err := rdb.Do(ctx, "CLIENT", "INFO").Result()
+               require.NoError(t, err)
+               require.Contains(t, info, "lib-name=lower-lib")
+       })
+
+       t.Run("CLIENT SETINFO with empty value clears the field", func(t 
*testing.T) {
+               res, err := rdb.Do(ctx, "CLIENT", "SETINFO", "LIB-NAME", 
"").Result()
+               require.NoError(t, err)
+               require.Equal(t, "OK", res)
+
+               info, err := rdb.Do(ctx, "CLIENT", "INFO").Result()
+               require.NoError(t, err)
+               require.Contains(t, info.(string), "lib-name=")
+               require.False(t, strings.Contains(info.(string), 
"lib-name=lower-lib"))
+       })
+
+       t.Run("CLIENT SETINFO rejects unknown attribute", func(t *testing.T) {
+               err := rdb.Do(ctx, "CLIENT", "SETINFO", "UNKNOWN", 
"value").Err()
+               require.Error(t, err)
+               require.Contains(t, err.Error(), "Unrecognized option")
+       })
+
+       t.Run("CLIENT SETINFO rejects value with spaces", func(t *testing.T) {
+               err := rdb.Do(ctx, "CLIENT", "SETINFO", "LIB-NAME", "my 
lib").Err()
+               require.Error(t, err)
+       })
+
+       t.Run("CLIENT SETINFO wrong number of arguments", func(t *testing.T) {
+               err := rdb.Do(ctx, "CLIENT", "SETINFO", "LIB-NAME").Err()
+               require.Error(t, err)
+       })
+}
diff --git a/tests/gocase/unit/monitor/monitor_test.go 
b/tests/gocase/unit/monitor/monitor_test.go
index a76a84a52..096f2e2ad 100644
--- a/tests/gocase/unit/monitor/monitor_test.go
+++ b/tests/gocase/unit/monitor/monitor_test.go
@@ -34,7 +34,7 @@ func TestMonitor(t *testing.T) {
        defer srv.Close()
 
        ctx := context.Background()
-       rdb := srv.NewClient()
+       rdb := srv.NewClientWithOption(&redis.Options{DisableIdentity: true})
        defer func() { require.NoError(t, rdb.Close()) }()
 
        t.Run("MONITOR can log executed commands", func(t *testing.T) {
@@ -85,7 +85,8 @@ func TestMonitorRedactPassword(t *testing.T) {
        c.MustRead(t, "+OK")
 
        rdb := srv.NewClientWithOption(&redis.Options{
-               Password: "testpass",
+               Password:        "testpass",
+               DisableIdentity: true,
        })
        defer func() { require.NoError(t, rdb.Close()) }()
 

Reply via email to