Skip to content

Commit 1682dcc

Browse files
authored
[queue] add Lag() command to measure consumer group lag (#159)
This adds a Lag() command to measure the lag of a consumer group over a queue. Lag is the number of messages in the queue that have not yet been picked up by a Read() command.
1 parent 3490956 commit 1682dcc

4 files changed

Lines changed: 81 additions & 1 deletion

File tree

‎queue/client.go‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ func (c *Client) Len(ctx context.Context, name string) (int64, error) {
4343
return lenScript.RunRO(ctx, c.rdb, []string{name}).Int64()
4444
}
4545

46+
// Lag calculates the aggregate lag (that is, number of messages that have not
47+
// yet been read) of the consumer group for the given queue. It adds up the
48+
// consumer group's lag of all the streams in the queue, based on the value
49+
// reported by XINFO GROUPS for the stream. If any of the streams report a
50+
// `nil` lag, Lag returns an error.
51+
//
52+
// As a side effect, Lag creates the consumer group if it does not already
53+
// exist.
54+
func (c *Client) Lag(ctx context.Context, queue string, group string) (int64, error) {
55+
return lagScript.Run(ctx, c.rdb, []string{queue}, group).Int64()
56+
}
57+
4658
// Read a single message from the queue. If the Block field of args is
4759
// non-zero, the call may block for up to that duration waiting for a new
4860
// message.

‎queue/client_test.go‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ func TestClientIntegration(t *testing.T) {
6666

6767
ids := make(map[string]struct{})
6868

69-
for range 15 {
69+
for i := range 15 {
70+
lag, err := client.Lag(ctx, "test", "mygroup")
71+
require.NoError(t, err)
72+
assert.EqualValues(t, 15-i, lag)
73+
7074
msg, err := client.Read(ctx, &queue.ReadArgs{
7175
Name: "test",
7276
Group: "mygroup",
@@ -88,7 +92,18 @@ func TestClientIntegration(t *testing.T) {
8892
Group: "mygroup",
8993
Consumer: "mygroup:123",
9094
})
95+
9196
require.ErrorIs(t, err, queue.Empty)
97+
98+
// Len remains 15 (because we haven't XDELed the messages or XTRIMed the stream)
99+
length, err = client.Len(ctx, "test")
100+
require.NoError(t, err)
101+
assert.EqualValues(t, 15, length)
102+
103+
// But Lag is now 0 (because we've XREADGROUPed everything)
104+
lag, err := client.Lag(ctx, "test", "mygroup")
105+
require.NoError(t, err)
106+
assert.EqualValues(t, 0, lag)
92107
}
93108

94109
// Check that the Block option works as expected

‎queue/lag.lua‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
-- Lag commands take the form
2+
--
3+
-- EVALSHA sha 1 key group
4+
--
5+
-- Note: strictly, it is illegal for a script to manipulate keys that are not
6+
-- explicitly passed to EVAL{,SHA}, but in practice this is fine as long as all
7+
-- keys are on the same server (e.g. in cluster scenarios). In our case a single
8+
-- queue, which may be composed of multiple streams and metadata keys, is always
9+
-- on the same server.
10+
11+
local base = KEYS[1]
12+
local group = ARGV[1]
13+
14+
local key_meta = base .. ':meta'
15+
16+
local streams = tonumber(redis.call('HGET', key_meta, 'streams') or 1)
17+
local result = 0
18+
19+
for idx = 0, streams-1 do
20+
local stream = base .. ':s' .. idx
21+
22+
-- the group must exist for us to measure its lag. we create it here; if it
23+
-- already exists this returns a BUSYGROUP error which we ignore
24+
redis.pcall('XGROUP', 'CREATE', stream, group, '0', 'MKSTREAM')
25+
26+
local info = redis.pcall('XINFO', 'GROUPS', stream)
27+
if info['err'] == 'ERR no such key' then
28+
-- if the stream doesn't exist, treat it as zero lag
29+
elseif info['err'] then
30+
return redis.error_reply(info['err']..' accessing '..stream)
31+
else
32+
for i,v in ipairs(info) do
33+
if v[2] == group then
34+
if not v[12] then
35+
-- lag can be nil; we propagate this to the caller
36+
return redis.error_reply('ERR unknown lag for group '..group..' on stream '..stream)
37+
end
38+
39+
result = result + v[12]
40+
break
41+
end
42+
end
43+
end
44+
end
45+
46+
return result

‎queue/queue.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ var (
4444
lenCmd string
4545
lenScript = redis.NewScript(lenCmd)
4646

47+
//go:embed lag.lua
48+
lagCmd string
49+
lagScript = redis.NewScript(lagCmd)
50+
4751
//go:embed read.lua
4852
readCmd string
4953
readScript = redis.NewScript(readCmd)
@@ -57,6 +61,9 @@ func prepare(ctx context.Context, rdb redis.Cmdable) error {
5761
if err := lenScript.Load(ctx, rdb).Err(); err != nil {
5862
return err
5963
}
64+
if err := lagScript.Load(ctx, rdb).Err(); err != nil {
65+
return err
66+
}
6067
if err := readScript.Load(ctx, rdb).Err(); err != nil {
6168
return err
6269
}

0 commit comments

Comments
 (0)