Skip to content

Commit 277e8b7

Browse files
ofekshenawachayim
andauthored
Support Monitor Command (#2830)
* Add monitor command * Add monitor commadn and tests * insure goroutine shutdown * fix data race * linting * change timeout explanation --------- Co-authored-by: Chayim <[email protected]>
1 parent 631deaf commit 277e8b7

File tree

4 files changed

+153
-1
lines changed

4 files changed

+153
-1
lines changed

command.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"regexp"
99
"strconv"
1010
"strings"
11+
"sync"
1112
"time"
1213

1314
"github.com/redis/go-redis/v9/internal"
@@ -5381,3 +5382,85 @@ func (cmd *InfoCmd) Item(section, key string) string {
53815382
return cmd.val[section][key]
53825383
}
53835384
}
5385+
5386+
type MonitorStatus int
5387+
5388+
const (
5389+
monitorStatusIdle MonitorStatus = iota
5390+
monitorStatusStart
5391+
monitorStatusStop
5392+
)
5393+
5394+
type MonitorCmd struct {
5395+
baseCmd
5396+
ch chan string
5397+
status MonitorStatus
5398+
mu sync.Mutex
5399+
}
5400+
5401+
func newMonitorCmd(ctx context.Context, ch chan string) *MonitorCmd {
5402+
return &MonitorCmd{
5403+
baseCmd: baseCmd{
5404+
ctx: ctx,
5405+
args: []interface{}{"monitor"},
5406+
},
5407+
ch: ch,
5408+
status: monitorStatusIdle,
5409+
mu: sync.Mutex{},
5410+
}
5411+
}
5412+
5413+
func (cmd *MonitorCmd) String() string {
5414+
return cmdString(cmd, nil)
5415+
}
5416+
5417+
func (cmd *MonitorCmd) readReply(rd *proto.Reader) error {
5418+
ctx, cancel := context.WithCancel(cmd.ctx)
5419+
go func(ctx context.Context) {
5420+
for {
5421+
select {
5422+
case <-ctx.Done():
5423+
return
5424+
default:
5425+
err := cmd.readMonitor(rd, cancel)
5426+
if err != nil {
5427+
cmd.err = err
5428+
return
5429+
}
5430+
}
5431+
}
5432+
}(ctx)
5433+
return nil
5434+
}
5435+
5436+
func (cmd *MonitorCmd) readMonitor(rd *proto.Reader, cancel context.CancelFunc) error {
5437+
for {
5438+
cmd.mu.Lock()
5439+
st := cmd.status
5440+
cmd.mu.Unlock()
5441+
if pk, _ := rd.Peek(1); len(pk) != 0 && st == monitorStatusStart {
5442+
line, err := rd.ReadString()
5443+
if err != nil {
5444+
return err
5445+
}
5446+
cmd.ch <- line
5447+
}
5448+
if st == monitorStatusStop {
5449+
cancel()
5450+
break
5451+
}
5452+
}
5453+
return nil
5454+
}
5455+
5456+
func (cmd *MonitorCmd) Start() {
5457+
cmd.mu.Lock()
5458+
defer cmd.mu.Unlock()
5459+
cmd.status = monitorStatusStart
5460+
}
5461+
5462+
func (cmd *MonitorCmd) Stop() {
5463+
cmd.mu.Lock()
5464+
defer cmd.mu.Unlock()
5465+
cmd.status = monitorStatusStop
5466+
}

commands.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,6 @@ type Cmdable interface {
204204
SlowLogGet(ctx context.Context, num int64) *SlowLogCmd
205205
Time(ctx context.Context) *TimeCmd
206206
DebugObject(ctx context.Context, key string) *StringCmd
207-
208207
MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd
209208

210209
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
@@ -700,3 +699,20 @@ func (c cmdable) ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *St
700699
_ = c(ctx, cmd)
701700
return cmd
702701
}
702+
703+
/*
704+
Monitor - represents a Redis MONITOR command, allowing the user to capture
705+
and process all commands sent to a Redis server. This mimics the behavior of
706+
MONITOR in the redis-cli.
707+
708+
Notes:
709+
- Using MONITOR blocks the connection to the server for itself. It needs a dedicated connection
710+
- The user should create a channel of type string
711+
- This runs concurrently in the background. Trigger via the Start and Stop functions
712+
See further: Redis MONITOR command: https://redis.io/commands/monitor
713+
*/
714+
func (c cmdable) Monitor(ctx context.Context, ch chan string) *MonitorCmd {
715+
cmd := newMonitorCmd(ctx, ch)
716+
_ = c(ctx, cmd)
717+
return cmd
718+
}

main_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ var (
4141
redisAddr = ":" + redisPort
4242
)
4343

44+
var (
45+
rediStackPort = "6379"
46+
rediStackAddr = ":" + rediStackPort
47+
)
48+
4449
var (
4550
sentinelAddrs = []string{":" + sentinelPort1, ":" + sentinelPort2, ":" + sentinelPort3}
4651

monitor_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package redis_test
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
. "github.com/bsm/ginkgo/v2"
8+
. "github.com/bsm/gomega"
9+
10+
"github.com/redis/go-redis/v9"
11+
)
12+
13+
var _ = Describe("Monitor command", Label("monitor"), func() {
14+
ctx := context.TODO()
15+
var client *redis.Client
16+
17+
BeforeEach(func() {
18+
client = redis.NewClient(&redis.Options{Addr: ":6379"})
19+
Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
20+
})
21+
22+
AfterEach(func() {
23+
Expect(client.Close()).NotTo(HaveOccurred())
24+
})
25+
26+
It("should monitor", Label("monitor"), func() {
27+
ress := make(chan string)
28+
client1 := redis.NewClient(&redis.Options{Addr: rediStackAddr})
29+
mn := client1.Monitor(ctx, ress)
30+
mn.Start()
31+
// Wait for the Redis server to be in monitoring mode.
32+
time.Sleep(100 * time.Millisecond)
33+
client.Set(ctx, "foo", "bar", 0)
34+
client.Set(ctx, "bar", "baz", 0)
35+
client.Set(ctx, "bap", 8, 0)
36+
client.Get(ctx, "bap")
37+
lst := []string{}
38+
for i := 0; i < 5; i++ {
39+
s := <-ress
40+
lst = append(lst, s)
41+
}
42+
mn.Stop()
43+
Expect(lst[0]).To(ContainSubstring("OK"))
44+
Expect(lst[1]).To(ContainSubstring(`"set" "foo" "bar"`))
45+
Expect(lst[2]).To(ContainSubstring(`"set" "bar" "baz"`))
46+
Expect(lst[3]).To(ContainSubstring(`"set" "bap" "8"`))
47+
})
48+
})

0 commit comments

Comments
 (0)