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
99 changes: 99 additions & 0 deletions redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,102 @@ type LatencyHistory struct {
// ExecutationTime is the amount of time needed for the command execution.
ExecutionTime time.Duration
}

// ClientFlags is redis-server client flags, copy from redis/src/server.h (redis 8.2)
type ClientFlags uint64

const (
ClientSlave ClientFlags = 1 << iota /* This client is a replica */
ClientMaster /* This client is a master */
ClientMonitor /* This client is a slave monitor, see MONITOR */
ClientMulti /* This client is in a MULTI context */
ClientBlocked /* The client is waiting in a blocking operation */
ClientDirtyCAS /* Watched keys modified. EXEC will fail. */
ClientCloseAfterReply /* Close after writing entire reply. */
ClientUnBlocked /* This client was unblocked and is stored in server.unblocked_clients */
ClientScript /* This is a non connected client used by Lua */
ClientAsking /* Client issued the ASKING command */
ClientCloseASAP /* Close this client ASAP */
ClientUnixSocket /* Client connected via Unix domain socket */
ClientDirtyExec /* EXEC will fail for errors while queueing */
ClientMasterForceReply /* Queue replies even if is master */
ClientForceAOF /* Force AOF propagation of current cmd. */
ClientForceRepl /* Force replication of current cmd. */
ClientPrePSync /* Instance don't understand PSYNC. */
ClientReadOnly /* Cluster client is in read-only state. */
ClientPubSub /* Client is in Pub/Sub mode. */
ClientPreventAOFProp /* Don't propagate to AOF. */
ClientPreventReplProp /* Don't propagate to slaves. */
ClientPreventProp ClientFlags = ClientPreventAOFProp | ClientPreventReplProp
ClientPendingWrite /* Client has output to send but a write handler is yet not installed. */
ClientReplyOff /* Don't send replies to client. */
ClientReplySkipNext /* Set CLIENT_REPLY_SKIP for next cmd */
ClientReplySkip /* Don't send just this reply. */
ClientLuaDebug /* Run EVAL in debug mode. */
ClientLuaDebugSync /* EVAL debugging without fork() */
ClientModule /* Non connected client used by some module. */
ClientProtected /* Client should not be freed for now. */
ClientExecutingCommand /* Indicates that the client is currently in the process of handling a command. usually this will be marked only during call() however, blocked clients might have this flag kept until they will try to reprocess the command. */
ClientPendingCommand /* Indicates the client has a fully parsed command ready for execution. */
ClientTracking /* Client enabled keys tracking in order to perform client side caching. */
ClientTrackingBrokenRedir /* Target client is invalid. */
ClientTrackingBCAST /* Tracking in BCAST mode. */
ClientTrackingOptIn /* Tracking in opt-in mode. */
ClientTrackingOptOut /* Tracking in opt-out mode. */
ClientTrackingCaching /* CACHING yes/no was given, depending on optin/optout mode. */
ClientTrackingNoLoop /* Don't send invalidation messages about writes performed by myself.*/
ClientInToTable /* This client is in the timeout table. */
ClientProtocolError /* Protocol error chatting with it. */
ClientCloseAfterCommand /* Close after executing commands and writing entire reply. */
ClientDenyBlocking /* Indicate that the client should not be blocked. currently, turned on inside MULTI, Lua, RM_Call, and AOF client */
ClientReplRDBOnly /* This client is a replica that only wants RDB without replication buffer. */
ClientNoEvict /* This client is protected against client memory eviction. */
ClientAllowOOM /* Client used by RM_Call is allowed to fully execute scripts even when in OOM */
ClientNoTouch /* This client will not touch LFU/LRU stats. */
ClientPushing /* This client is pushing notifications. */
ClientModuleAuthHasResult /* Indicates a client in the middle of module based auth had been authenticated from the Module. */
ClientModulePreventAOFProp /* Module client do not want to propagate to AOF */
ClientModulePreventReplProp /* Module client do not want to propagate to replica */
ClientReExecutingCommand /* The client is re-executing the command. */
ClientReplRDBChannel /* Client which is used for RDB delivery as part of RDB channel replication */
ClientInternal /* Internal client connection */
)

// Client represents a redis-server client.
type Client struct {
ID int64 // redis version 2.8.12, a unique 64-bit client ID
Addr string // address/port of the client
LAddr string // address/port of local address client connected to (bind address)
FD int64 // file descriptor corresponding to the socket
Name string // the name set by the client with CLIENT SETNAME
Age time.Duration // total duration of the connection in seconds
Idle time.Duration // idle time of the connection in seconds
Flags ClientFlags // client flags (see below)
DB int // current database ID
Sub int // number of channel subscriptions
PSub int // number of pattern matching subscriptions
SSub int // number of shard channel subscriptions. Added in Redis 7.0.3
Multi int // number of commands in a MULTI/EXEC context
Watch int // number of keys this client is currently watching. Added in Redis 7.4
QueryBuf int // qbuf, query buffer length (0 means no query pending)
QueryBufFree int // qbuf-free, free space of the query buffer (0 means the buffer is full)
ArgvMem int // incomplete arguments for the next command (already extracted from query buffer)
MultiMem int // memory is used up by buffered multi commands. Added in Redis 7.0
BufferSize int // rbs, current size of the client's read buffer in bytes. Added in Redis 7.0
BufferPeak int // rbp, peak size of the client's read buffer since the client connected. Added in Redis 7.0
OutputBufferLength int // obl, output buffer length
OutputListLength int // oll, output list length (replies are queued in this list when the buffer is full)
OutputMemory int // omem, output buffer memory usage
TotalMemory int // tot-mem, total memory consumed by this client in its various buffers
Events string // file descriptor events (see below)
LastCmd string // cmd, last command played
User string // the authenticated username of the client
Redir int64 // client id of current client tracking redirection
Resp int // client RESP protocol version. Added in Redis 7.0
LibName string // client library name. Added in Redis 7.2
LibVer string // client library version. Added in Redis 7.2
IoThread int // io-thread, id of I/O thread assigned to the client. Added in Redis 8.0
TotalNetIn int // tot-net-in, total network input bytes read from this client.
TotalNetOut int // tot-net-out, total network output bytes sent to this client.
TotalCmds int // tot-cmds, total count of commands this client executed.
}
157 changes: 157 additions & 0 deletions redis/reply.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -733,3 +734,159 @@ func LatencyHistories(result interface{}, err error) ([]LatencyHistory, error) {

return latencyHistories, nil
}

// ClientInfo is a helper that parse the CLIENT INFO command output and
// returns a Client struct.
func ClientInfo(result interface{}, err error) (*Client, error) {
Comment thread
destinyoooo marked this conversation as resolved.
rawInfo, err := String(result, err)
if err != nil {
return nil, fmt.Errorf("redigo: client info is not a string: %w", err)
}

return parseClientInfo(strings.TrimSpace(rawInfo))
}

func parseClientInfo(input string) (*Client, error) {
info := &Client{}
var err error
for _, s := range strings.Split(input, " ") {
kv := strings.Split(s, "=")
if len(kv) != 2 {
return nil, fmt.Errorf("redigo: unexpected client info data (%s)", s)
}
key, val := kv[0], kv[1]

switch key {
case "id":
info.ID, err = strconv.ParseInt(val, 10, 64)
case "addr":
info.Addr = val
case "laddr":
info.LAddr = val
case "fd":
info.FD, err = strconv.ParseInt(val, 10, 64)
case "name":
info.Name = val
case "age":
var age int
if age, err = strconv.Atoi(val); err == nil {
info.Age = time.Duration(age) * time.Second
}
case "idle":
var idle int
if idle, err = strconv.Atoi(val); err == nil {
info.Idle = time.Duration(idle) * time.Second
}
case "flags":
if val == "N" {
break
}
for i := 0; i < len(val); i++ {
switch val[i] {
case 'S':
info.Flags |= ClientSlave
case 'O':
info.Flags |= ClientSlave | ClientMonitor
case 'M':
info.Flags |= ClientMaster
case 'P':
info.Flags |= ClientPubSub
case 'x':
info.Flags |= ClientMulti
case 'b':
info.Flags |= ClientBlocked
case 't':
info.Flags |= ClientTracking
case 'R':
info.Flags |= ClientTrackingBrokenRedir
case 'B':
info.Flags |= ClientTrackingBCAST
case 'd':
info.Flags |= ClientDirtyCAS
case 'c':
info.Flags |= ClientCloseAfterCommand
case 'u':
info.Flags |= ClientUnBlocked
case 'A':
info.Flags |= ClientCloseASAP
case 'U':
info.Flags |= ClientUnixSocket
case 'r':
info.Flags |= ClientReadOnly
case 'e':
info.Flags |= ClientNoEvict
case 'T':
info.Flags |= ClientNoTouch
case 'C':
info.Flags |= ClientReplRDBChannel
case 'I':
info.Flags |= ClientInternal
case 'N':
// ignore
default:
return nil, fmt.Errorf("redigo: unexpected client info flags(%s)", string(val[i]))
}
}
case "db":
info.DB, err = strconv.Atoi(val)
case "sub":
info.Sub, err = strconv.Atoi(val)
case "psub":
info.PSub, err = strconv.Atoi(val)
case "ssub":
info.SSub, err = strconv.Atoi(val)
case "multi":
info.Multi, err = strconv.Atoi(val)
case "watch":
info.Watch, err = strconv.Atoi(val)
case "qbuf":
info.QueryBuf, err = strconv.Atoi(val)
case "qbuf-free":
info.QueryBufFree, err = strconv.Atoi(val)
case "argv-mem":
info.ArgvMem, err = strconv.Atoi(val)
case "multi-mem":
info.MultiMem, err = strconv.Atoi(val)
case "rbs":
info.BufferSize, err = strconv.Atoi(val)
case "rbp":
info.BufferPeak, err = strconv.Atoi(val)
case "obl":
info.OutputBufferLength, err = strconv.Atoi(val)
case "oll":
info.OutputListLength, err = strconv.Atoi(val)
case "omem":
info.OutputMemory, err = strconv.Atoi(val)
case "tot-mem":
info.TotalMemory, err = strconv.Atoi(val)
case "events":
info.Events = val
case "cmd":
info.LastCmd = val
case "user":
info.User = val
case "redir":
info.Redir, err = strconv.ParseInt(val, 10, 64)
case "resp":
info.Resp, err = strconv.Atoi(val)
case "lib-name":
info.LibName = val
case "lib-ver":
info.LibVer = val
case "io-thread":
info.IoThread, err = strconv.Atoi(val)
case "tot-net-in":
info.TotalNetIn, err = strconv.Atoi(val)
case "tot-net-out":
info.TotalNetOut, err = strconv.Atoi(val)
case "tot-cmds":
info.TotalCmds, err = strconv.Atoi(val)
default:
return nil, fmt.Errorf("redigo: unexpected client info key(%s)", key)
}
if err != nil {
return nil, fmt.Errorf("redigo: invalid client info value(%s): %w", val, err)
}
}
return info, nil
}
18 changes: 18 additions & 0 deletions redis/reply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,24 @@ func TestLatencyHistories(t *testing.T) {
require.GreaterOrEqual(t, latencyEvent.ExecutionTime, time.Millisecond)
}

func TestCommandsInfo(t *testing.T) {
connectionName := "local-client"
c, err := redis.DialDefaultServer(redis.DialClientName(connectionName))
require.NoError(t, err)
defer c.Close()

reply, err = c.Do("client", "info")
require.NoError(t, err)

result, err := redis.ClientInfo(reply, err)
require.NoError(t, err)

require.Equal(t, result.Name, "local-client")
require.Equal(t, result.User, "default")
require.Equal(t, result.Events, "r")
require.Equal(t, result.DB, 0)
}

// dial wraps DialDefaultServer() with a more suitable function name for examples.
func dial() (redis.Conn, error) {
return redis.DialDefaultServer()
Expand Down