2
0
mirror of https://github.com/hibiken/asynq.git synced 2024-12-26 15:52:18 +08:00

Clear group if aggregation set empties the group

This commit is contained in:
Ken Hibino 2022-03-13 06:44:42 -07:00
parent 60a4dc1401
commit 74d2eea4e0
2 changed files with 230 additions and 129 deletions

View File

@ -993,16 +993,26 @@ func (r *RDB) ListGroups(qname string) ([]string, error) {
return groups, nil return groups, nil
} }
// TODO: Add comment describing what the script does. // aggregationCheckCmd checks the given group for whether to create an aggregation set.
// An aggregation set is created if one of the aggregation criteria is met:
// 1) group has reached or exceeded its max size
// 2) group's oldest task has reached or exceeded its max delay
// 3) group's latest task has reached or exceeded its grace period
// if aggreation criteria is met, the command moves those tasks from the group
// and put them in an aggregation set. Additionally, if the creation of aggregation set
// empties the group, it will clear the group name from the all groups set.
//
// KEYS[1] -> asynq:{<qname>}:g:<gname> // KEYS[1] -> asynq:{<qname>}:g:<gname>
// KEYS[2] -> asynq:{<qname>}:g:<gname>:<aggregation_set_id> // KEYS[2] -> asynq:{<qname>}:g:<gname>:<aggregation_set_id>
// KEYS[3] -> asynq:{<qname>}:aggregation_sets // KEYS[3] -> asynq:{<qname>}:aggregation_sets
// KEYS[4] -> asynq:{<qname>}:groups
// ------- // -------
// ARGV[1] -> max group size // ARGV[1] -> max group size
// ARGV[2] -> max group delay in unix time // ARGV[2] -> max group delay in unix time
// ARGV[3] -> start time of the grace period // ARGV[3] -> start time of the grace period
// ARGV[4] -> aggregation set expire time // ARGV[4] -> aggregation set expire time
// ARGV[5] -> current time in unix time // ARGV[5] -> current time in unix time
// ARGV[6] -> group name
// //
// Output: // Output:
// Returns 0 if no aggregation set was created // Returns 0 if no aggregation set was created
@ -1020,6 +1030,9 @@ if maxSize ~= 0 and size >= maxSize then
end end
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1) redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2]) redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
if size == maxSize then
redis.call("SREM", KEYS[4], ARGV[6])
end
return 1 return 1
end end
local maxDelay = tonumber(ARGV[2]) local maxDelay = tonumber(ARGV[2])
@ -1035,6 +1048,9 @@ if maxDelay ~= 0 then
end end
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1) redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2]) redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
if size <= maxSize then
redis.call("SREM", KEYS[4], ARGV[6])
end
return 1 return 1
end end
end end
@ -1048,6 +1064,9 @@ if latestEntryScore <= gracePeriodStartTime then
end end
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1) redis.call("ZREMRANGEBYRANK", KEYS[1], 0, maxSize-1)
redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2]) redis.call("ZADD", KEYS[3], ARGV[4], KEYS[2])
if size <= maxSize then
redis.call("SREM", KEYS[4], ARGV[6])
end
return 1 return 1
end end
return 0 return 0
@ -1072,6 +1091,7 @@ func (r *RDB) AggregationCheck(qname, gname string, t time.Time, gracePeriod, ma
base.GroupKey(qname, gname), base.GroupKey(qname, gname),
base.AggregationSetKey(qname, gname, aggregationSetID), base.AggregationSetKey(qname, gname, aggregationSetID),
base.AllAggregationSets(qname), base.AllAggregationSets(qname),
base.AllGroups(qname),
} }
argv := []interface{}{ argv := []interface{}{
maxSize, maxSize,
@ -1079,6 +1099,7 @@ func (r *RDB) AggregationCheck(qname, gname string, t time.Time, gracePeriod, ma
int64(gracePeriod.Seconds()), int64(gracePeriod.Seconds()),
expireTime.Unix(), expireTime.Unix(),
t.Unix(), t.Unix(),
gname,
} }
n, err := r.runScriptWithErrorCode(context.Background(), op, aggregationCheckCmd, keys, argv...) n, err := r.runScriptWithErrorCode(context.Background(), op, aggregationCheckCmd, keys, argv...)
if err != nil { if err != nil {

View File

@ -3112,6 +3112,7 @@ func TestAggregationCheck(t *testing.T) {
now := time.Now() now := time.Now()
r.SetClock(timeutil.NewSimulatedClock(now)) r.SetClock(timeutil.NewSimulatedClock(now))
ctx := context.Background()
msg1 := h.NewTaskMessageBuilder().SetType("task1").SetGroup("mygroup").Build() msg1 := h.NewTaskMessageBuilder().SetType("task1").SetGroup("mygroup").Build()
msg2 := h.NewTaskMessageBuilder().SetType("task2").SetGroup("mygroup").Build() msg2 := h.NewTaskMessageBuilder().SetType("task2").SetGroup("mygroup").Build()
msg3 := h.NewTaskMessageBuilder().SetType("task3").SetGroup("mygroup").Build() msg3 := h.NewTaskMessageBuilder().SetType("task3").SetGroup("mygroup").Build()
@ -3119,23 +3120,33 @@ func TestAggregationCheck(t *testing.T) {
msg5 := h.NewTaskMessageBuilder().SetType("task5").SetGroup("mygroup").Build() msg5 := h.NewTaskMessageBuilder().SetType("task5").SetGroup("mygroup").Build()
tests := []struct { tests := []struct {
desc string desc string
groups map[string]map[string][]base.Z // initial data
qname string tasks []*taskData
gname string groups map[string][]*redis.Z
gracePeriod time.Duration allGroups map[string][]string
maxDelay time.Duration
maxSize int // args
qname string
gname string
gracePeriod time.Duration
maxDelay time.Duration
maxSize int
// expectaions
shouldCreateSet bool // whether the check should create a new aggregation set shouldCreateSet bool // whether the check should create a new aggregation set
wantAggregationSet []*base.TaskMessage wantAggregationSet []*base.TaskMessage
wantGroups map[string]map[string][]base.Z wantGroups map[string][]redis.Z
shouldClearGroup bool // whehter the check should clear the group from redis
}{ }{
{ {
desc: "with an empty group", desc: "with an empty group",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{},
"default": { groups: map[string][]*redis.Z{
"mygroup": {}, base.GroupKey("default", "mygroup"): {},
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {},
}, },
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
@ -3144,25 +3155,32 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 5, maxSize: 5,
shouldCreateSet: false, shouldCreateSet: false,
wantAggregationSet: nil, wantAggregationSet: nil,
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {},
"mygroup": {},
},
}, },
shouldClearGroup: true,
}, },
{ {
desc: "with a group size reaching the max size", desc: "with a group size reaching the max size",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-5 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, {msg: msg4, state: base.TaskStateAggregating},
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, {msg: msg5, state: base.TaskStateAggregating},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, },
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 1 * time.Minute, gracePeriod: 1 * time.Minute,
@ -3170,25 +3188,32 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 5, maxSize: 5,
shouldCreateSet: true, shouldCreateSet: true,
wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3, msg4, msg5}, wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3, msg4, msg5},
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {},
"mygroup": {},
},
}, },
shouldClearGroup: true,
}, },
{ {
desc: "with group size greater than max size", desc: "with group size greater than max size",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-5 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, {msg: msg4, state: base.TaskStateAggregating},
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, {msg: msg5, state: base.TaskStateAggregating},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, },
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 2 * time.Minute, gracePeriod: 2 * time.Minute,
@ -3196,26 +3221,31 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 3, maxSize: 3,
shouldCreateSet: true, shouldCreateSet: true,
wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3}, wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3},
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {
"mygroup": { {Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, {Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()},
},
}, },
}, },
shouldClearGroup: false,
}, },
{ {
desc: "with the most recent task older than grace period", desc: "with the most recent task older than grace period",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-5 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, },
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-5 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 1 * time.Minute, gracePeriod: 1 * time.Minute,
@ -3223,25 +3253,32 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 5, maxSize: 5,
shouldCreateSet: true, shouldCreateSet: true,
wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3}, wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3},
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {},
"mygroup": {},
},
}, },
shouldClearGroup: true,
}, },
{ {
desc: "with the oldest task older than max delay", desc: "with the oldest task older than max delay",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-15 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, {msg: msg4, state: base.TaskStateAggregating},
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, {msg: msg5, state: base.TaskStateAggregating},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, },
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 2 * time.Minute, gracePeriod: 2 * time.Minute,
@ -3249,25 +3286,32 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 30, maxSize: 30,
shouldCreateSet: true, shouldCreateSet: true,
wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3, msg4, msg5}, wantAggregationSet: []*base.TaskMessage{msg1, msg2, msg3, msg4, msg5},
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {},
"mygroup": {},
},
}, },
shouldClearGroup: true,
}, },
{ {
desc: "with unlimited size", desc: "with unlimited size",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-15 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, {msg: msg4, state: base.TaskStateAggregating},
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, {msg: msg5, state: base.TaskStateAggregating},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, },
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 1 * time.Minute, gracePeriod: 1 * time.Minute,
@ -3275,25 +3319,38 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 0, // maxSize=0 indicates no size limit maxSize: 0, // maxSize=0 indicates no size limit
shouldCreateSet: false, shouldCreateSet: false,
wantAggregationSet: nil, wantAggregationSet: nil,
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {
"mygroup": {}, {Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
shouldClearGroup: false,
}, },
{ {
desc: "with unlimited delay", desc: "with unlimited delay",
groups: map[string]map[string][]base.Z{ tasks: []*taskData{
"default": { {msg: msg1, state: base.TaskStateAggregating},
"mygroup": { {msg: msg2, state: base.TaskStateAggregating},
{Message: msg1, Score: now.Add(-15 * time.Minute).Unix()}, {msg: msg3, state: base.TaskStateAggregating},
{Message: msg2, Score: now.Add(-3 * time.Minute).Unix()}, {msg: msg4, state: base.TaskStateAggregating},
{Message: msg3, Score: now.Add(-2 * time.Minute).Unix()}, {msg: msg5, state: base.TaskStateAggregating},
{Message: msg4, Score: now.Add(-1 * time.Minute).Unix()}, },
{Message: msg5, Score: now.Add(-10 * time.Second).Unix()}, groups: map[string][]*redis.Z{
}, base.GroupKey("default", "mygroup"): {
{Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
allGroups: map[string][]string{
base.AllGroups("default"): {"mygroup"},
},
qname: "default", qname: "default",
gname: "mygroup", gname: "mygroup",
gracePeriod: 1 * time.Minute, gracePeriod: 1 * time.Minute,
@ -3301,58 +3358,71 @@ func TestAggregationCheck(t *testing.T) {
maxSize: 10, maxSize: 10,
shouldCreateSet: false, shouldCreateSet: false,
wantAggregationSet: nil, wantAggregationSet: nil,
wantGroups: map[string]map[string][]base.Z{ wantGroups: map[string][]redis.Z{
"default": { base.GroupKey("default", "mygroup"): {
"mygroup": {}, {Member: msg1.ID, Score: float64(now.Add(-15 * time.Minute).Unix())},
{Member: msg2.ID, Score: float64(now.Add(-3 * time.Minute).Unix())},
{Member: msg3.ID, Score: float64(now.Add(-2 * time.Minute).Unix())},
{Member: msg4.ID, Score: float64(now.Add(-1 * time.Minute).Unix())},
{Member: msg5.ID, Score: float64(now.Add(-10 * time.Second).Unix())},
}, },
}, },
shouldClearGroup: false,
}, },
} }
for _, tc := range tests { for _, tc := range tests {
h.FlushDB(t, r.client) h.FlushDB(t, r.client)
h.SeedAllGroups(t, r.client, tc.groups)
aggregationSetID, err := r.AggregationCheck(tc.qname, tc.gname, now, tc.gracePeriod, tc.maxDelay, tc.maxSize) t.Run(tc.desc, func(t *testing.T) {
if err != nil { SeedTasks(t, r.client, tc.tasks)
t.Errorf("%s: AggregationCheck returned error: %v", tc.desc, err) SeedZSets(t, r.client, tc.groups)
continue SeedSets(t, r.client, tc.allGroups)
}
if !tc.shouldCreateSet && aggregationSetID != "" { aggregationSetID, err := r.AggregationCheck(tc.qname, tc.gname, now, tc.gracePeriod, tc.maxDelay, tc.maxSize)
t.Errorf("%s: AggregationCheck returned non empty set ID. want empty ID", tc.desc) if err != nil {
continue t.Fatalf("AggregationCheck returned error: %v", err)
} }
if tc.shouldCreateSet && aggregationSetID == "" {
t.Errorf("%s: AggregationCheck returned empty set ID. want non empty ID", tc.desc)
continue
}
if !tc.shouldCreateSet { if !tc.shouldCreateSet && aggregationSetID != "" {
continue // below checks are intended for aggregation set t.Fatal("AggregationCheck returned non empty set ID. want empty ID")
} }
if tc.shouldCreateSet && aggregationSetID == "" {
t.Fatal("AggregationCheck returned empty set ID. want non empty ID")
}
msgs, deadline, err := r.ReadAggregationSet(tc.qname, tc.gname, aggregationSetID) if tc.shouldCreateSet {
if err != nil { msgs, deadline, err := r.ReadAggregationSet(tc.qname, tc.gname, aggregationSetID)
t.Fatalf("%s: Failed to read aggregation set %q: %v", tc.desc, aggregationSetID, err) if err != nil {
} t.Fatalf("Failed to read aggregation set %q: %v", aggregationSetID, err)
if diff := cmp.Diff(tc.wantAggregationSet, msgs, h.SortMsgOpt); diff != "" { }
t.Errorf("%s: Mismatch found in aggregation set: (-want,+got)\n%s", tc.desc, diff) if diff := cmp.Diff(tc.wantAggregationSet, msgs, h.SortMsgOpt); diff != "" {
} t.Errorf("Mismatch found in aggregation set: (-want,+got)\n%s", diff)
}
if wantDeadline := now.Add(aggregationTimeout); deadline.Unix() != wantDeadline.Unix() { if wantDeadline := now.Add(aggregationTimeout); deadline.Unix() != wantDeadline.Unix() {
t.Errorf("%s: ReadAggregationSet returned deadline=%v, want=%v", tc.desc, deadline, wantDeadline) t.Errorf("ReadAggregationSet returned deadline=%v, want=%v", deadline, wantDeadline)
}
for qname, groups := range tc.wantGroups {
for gname, want := range groups {
gotGroup := h.GetGroupEntries(t, r.client, qname, gname)
if diff := cmp.Diff(want, gotGroup, h.SortZSetEntryOpt); diff != "" {
t.Errorf("%s: Mismatch found in group zset: %q: (-want,+got)\n%s",
tc.desc, base.GroupKey(qname, gname), diff)
} }
} }
}
AssertZSets(t, r.client, tc.wantGroups)
if tc.shouldClearGroup {
if key := base.GroupKey(tc.qname, tc.gname); r.client.Exists(ctx, key).Val() != 0 {
t.Errorf("group key %q still exists", key)
}
if r.client.SIsMember(ctx, base.AllGroups(tc.qname), tc.gname).Val() {
t.Errorf("all-group set %q still contains the group name %q", base.AllGroups(tc.qname), tc.gname)
}
} else {
if key := base.GroupKey(tc.qname, tc.gname); r.client.Exists(ctx, key).Val() == 0 {
t.Errorf("group key %q does not exists", key)
}
if !r.client.SIsMember(ctx, base.AllGroups(tc.qname), tc.gname).Val() {
t.Errorf("all-group set %q doesn't contains the group name %q", base.AllGroups(tc.qname), tc.gname)
}
}
})
} }
} }
@ -3704,6 +3774,16 @@ func SeedZSets(tb testing.TB, r redis.UniversalClient, zsets map[string][]*redis
} }
} }
func SeedSets(tb testing.TB, r redis.UniversalClient, sets map[string][]string) {
for key, set := range sets {
for _, mem := range set {
if err := r.SAdd(context.Background(), key, mem).Err(); err != nil {
tb.Fatalf("Failed to seed set (key=%q): %v", key, err)
}
}
}
}
// TODO: move this helper somewhere more canonical // TODO: move this helper somewhere more canonical
func AssertZSets(t *testing.T, r redis.UniversalClient, wantZSets map[string][]redis.Z) { func AssertZSets(t *testing.T, r redis.UniversalClient, wantZSets map[string][]redis.Z) {
for key, want := range wantZSets { for key, want := range wantZSets {