mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-31 09:20:50 +01:00
ListDirectoryEntries fetched every member with its own GET, so a page of 1024 entries cost 1024 round trips. Fetch the values in pipelined batches of 128 as the loop reaches them and walk the replies in order. Each reply keeps its own error, so missing and expired members are cleaned up and skipped exactly as before, and an unreadable member still ends the listing at that name. Batching keeps a caller that lists with a huge limit and stops after a few entries from reading the whole directory. Co-authored-by: Patrick Schmidt <1310072+Woellchen@users.noreply.github.com> Co-authored-by: user.email <chrislusf@users.noreply.github.com>
421 lines
14 KiB
Go
421 lines
14 KiB
Go
package redis2
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func newTestStore(t *testing.T, keyPrefix string) (*UniversalRedis2Store, util.FullPath) {
|
|
t.Helper()
|
|
|
|
if os.Getenv("RUN_REDIS_TESTS") != "1" {
|
|
t.Skip("redis2 tests are disabled. Start a redis-server and set RUN_REDIS_TESTS=1 to enable, REDIS_ADDR defaults to 127.0.0.1:6379.")
|
|
}
|
|
|
|
addr := os.Getenv("REDIS_ADDR")
|
|
if addr == "" {
|
|
addr = "127.0.0.1:6379"
|
|
}
|
|
|
|
ctx := context.Background()
|
|
client := redis.NewClient(&redis.Options{Addr: addr})
|
|
t.Cleanup(func() {
|
|
if err := client.Close(); err != nil {
|
|
t.Errorf("close redis client: %v", err)
|
|
}
|
|
})
|
|
if err := client.Ping(ctx).Err(); err != nil {
|
|
t.Fatalf("connect to redis at %s: %v", addr, err)
|
|
}
|
|
|
|
store := &UniversalRedis2Store{Client: client, keyPrefix: keyPrefix}
|
|
|
|
dir := util.FullPath(fmt.Sprintf("/redis2_test_%d", time.Now().UnixNano()))
|
|
t.Cleanup(func() {
|
|
if err := store.DeleteFolderChildren(ctx, dir); err != nil {
|
|
t.Errorf("cleanup %s children: %v", dir, err)
|
|
}
|
|
if err := store.DeleteEntry(ctx, dir); err != nil {
|
|
t.Errorf("cleanup %s: %v", dir, err)
|
|
}
|
|
})
|
|
|
|
return store, dir
|
|
}
|
|
|
|
func insertTestEntry(t *testing.T, store *UniversalRedis2Store, path util.FullPath, ttlSec int32) {
|
|
t.Helper()
|
|
|
|
now := time.Now()
|
|
if err := store.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: path,
|
|
Attr: filer.Attr{Crtime: now, Mtime: now, Mode: 0644, TtlSec: ttlSec},
|
|
}); err != nil {
|
|
t.Fatalf("InsertEntry %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func listNames(t *testing.T, store *UniversalRedis2Store, dir util.FullPath) []string {
|
|
t.Helper()
|
|
|
|
names := []string{}
|
|
if _, err := store.ListDirectoryEntries(context.Background(), dir, "", true, 100, func(entry *filer.Entry) (bool, error) {
|
|
_, name := entry.DirAndName()
|
|
names = append(names, name)
|
|
return true, nil
|
|
}); err != nil {
|
|
t.Fatalf("ListDirectoryEntries %s: %v", dir, err)
|
|
}
|
|
return names
|
|
}
|
|
|
|
func indexMembers(t *testing.T, store *UniversalRedis2Store, dir util.FullPath) []string {
|
|
t.Helper()
|
|
|
|
members, err := store.Client.ZRangeByLex(context.Background(), store.getKey(genDirectoryListKey(string(dir))), &redis.ZRangeBy{Min: "-", Max: "+"}).Result()
|
|
if err != nil {
|
|
t.Fatalf("read directory index of %s: %v", dir, err)
|
|
}
|
|
return members
|
|
}
|
|
|
|
func TestListDirectoryEntriesRemovesOrphanedIndexMembers(t *testing.T) {
|
|
for _, keyPrefix := range []string{"", "sw:"} {
|
|
t.Run("keyPrefix="+keyPrefix, func(t *testing.T) {
|
|
store, dir := newTestStore(t, keyPrefix)
|
|
|
|
insertTestEntry(t, store, dir.Child("alive"), 0)
|
|
insertTestEntry(t, store, dir.Child("orphan"), 0)
|
|
|
|
if err := store.Client.Del(context.Background(), store.getKey(string(dir.Child("orphan")))).Err(); err != nil {
|
|
t.Fatalf("drop value key: %v", err)
|
|
}
|
|
|
|
if names := listNames(t, store, dir); len(names) != 1 || names[0] != "alive" {
|
|
t.Fatalf("listed %v, want [alive]", names)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 1 || members[0] != "alive" {
|
|
t.Fatalf("directory index holds %v, want [alive]", members)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveOrphanedDirectoryListMemberKeepsRecreatedEntry(t *testing.T) {
|
|
for _, keyPrefix := range []string{"", "sw:"} {
|
|
t.Run("keyPrefix="+keyPrefix, func(t *testing.T) {
|
|
store, dir := newTestStore(t, keyPrefix)
|
|
|
|
path := dir.Child("recreated")
|
|
insertTestEntry(t, store, path, 0)
|
|
|
|
store.removeOrphanedDirectoryListMember(context.Background(), dir, "recreated")
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 1 || members[0] != "recreated" {
|
|
t.Fatalf("directory index holds %v, want [recreated]", members)
|
|
}
|
|
|
|
if names := listNames(t, store, dir); len(names) != 1 || names[0] != "recreated" {
|
|
t.Fatalf("listed %v, want [recreated]", names)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveOrphanedDirectoryListMemberKeepsDirectoryWithChildren(t *testing.T) {
|
|
for _, keyPrefix := range []string{"", "sw:"} {
|
|
t.Run("keyPrefix="+keyPrefix, func(t *testing.T) {
|
|
store, dir := newTestStore(t, keyPrefix)
|
|
|
|
sub := dir.Child("sub")
|
|
insertTestEntry(t, store, sub, 0)
|
|
insertTestEntry(t, store, sub.Child("kid"), 0)
|
|
defer func() {
|
|
if err := store.DeleteFolderChildren(context.Background(), sub); err != nil {
|
|
t.Errorf("cleanup %s children: %v", sub, err)
|
|
}
|
|
}()
|
|
|
|
// evict the directory's own value while its child index is live
|
|
if err := store.Client.Del(context.Background(), store.getKey(string(sub))).Err(); err != nil {
|
|
t.Fatalf("drop value key: %v", err)
|
|
}
|
|
|
|
if names := listNames(t, store, dir); len(names) != 0 {
|
|
t.Fatalf("listed %v, want none", names)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 1 || members[0] != "sub" {
|
|
t.Fatalf("directory index holds %v, want [sub]", members)
|
|
}
|
|
|
|
if exists, err := store.Client.Exists(context.Background(), store.getKey(string(sub.Child("kid")))).Result(); err != nil || exists != 1 {
|
|
t.Fatalf("child value key exists=%d err=%v, want it kept", exists, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveOrphanedDirectoryListMemberSkipsSuperLargeDirectory(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
store.loadSuperLargeDirectories([]string{string(dir)})
|
|
|
|
// a member left from before the directory became super large
|
|
if err := store.Client.ZAdd(context.Background(), store.getKey(genDirectoryListKey(string(dir))), redis.Z{Score: 0, Member: "legacy"}).Err(); err != nil {
|
|
t.Fatalf("plant legacy member: %v", err)
|
|
}
|
|
|
|
store.removeOrphanedDirectoryListMember(context.Background(), dir, "legacy")
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 1 || members[0] != "legacy" {
|
|
t.Fatalf("directory index holds %v, want [legacy] untouched", members)
|
|
}
|
|
}
|
|
|
|
func requireValueKey(t *testing.T, store *UniversalRedis2Store, path util.FullPath) {
|
|
t.Helper()
|
|
|
|
if exists, err := store.Client.Exists(context.Background(), store.getKey(string(path))).Result(); err != nil || exists != 1 {
|
|
t.Fatalf("value key %s exists=%d err=%v, want it present", path, exists, err)
|
|
}
|
|
}
|
|
|
|
func TestListDirectoryEntriesRemovesIndexMembersExpiredByRedis(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
|
|
insertTestEntry(t, store, dir.Child("ttl"), 1)
|
|
requireValueKey(t, store, dir.Child("ttl"))
|
|
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for {
|
|
exists, err := store.Client.Exists(context.Background(), store.getKey(string(dir.Child("ttl")))).Result()
|
|
if err != nil {
|
|
t.Fatalf("check value key: %v", err)
|
|
}
|
|
if exists == 0 {
|
|
break
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("redis did not expire the value key, the logical expiry path is not being bypassed")
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
|
|
if names := listNames(t, store, dir); len(names) != 0 {
|
|
t.Fatalf("listed %v, want none", names)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 0 {
|
|
t.Fatalf("directory index holds %v, want none", members)
|
|
}
|
|
}
|
|
|
|
// logically expired (Crtime + TtlSec long past) while the physical key survives,
|
|
// because the redis TTL re-arms from the SET time
|
|
func insertLogicallyExpiredTestEntry(t *testing.T, store *UniversalRedis2Store, path util.FullPath) {
|
|
t.Helper()
|
|
|
|
created := time.Now().Add(-time.Hour)
|
|
if err := store.InsertEntry(context.Background(), &filer.Entry{
|
|
FullPath: path,
|
|
Attr: filer.Attr{Crtime: created, Mtime: created, Mode: 0644, TtlSec: 60},
|
|
}); err != nil {
|
|
t.Fatalf("InsertEntry %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
func TestListDirectoryEntriesDeletesLogicallyExpiredEntries(t *testing.T) {
|
|
for _, keyPrefix := range []string{"", "sw:"} {
|
|
t.Run("keyPrefix="+keyPrefix, func(t *testing.T) {
|
|
store, dir := newTestStore(t, keyPrefix)
|
|
|
|
insertLogicallyExpiredTestEntry(t, store, dir.Child("stale"))
|
|
requireValueKey(t, store, dir.Child("stale"))
|
|
|
|
if names := listNames(t, store, dir); len(names) != 0 {
|
|
t.Fatalf("listed %v, want none", names)
|
|
}
|
|
|
|
if exists, err := store.Client.Exists(context.Background(), store.getKey(string(dir.Child("stale")))).Result(); err != nil || exists != 0 {
|
|
t.Fatalf("value key exists=%d err=%v, want it deleted", exists, err)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 0 {
|
|
t.Fatalf("directory index holds %v, want none", members)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListDirectoryEntriesSkipsMissingAndExpiredMembersInOrder(t *testing.T) {
|
|
for _, keyPrefix := range []string{"", "sw:"} {
|
|
t.Run("keyPrefix="+keyPrefix, func(t *testing.T) {
|
|
store, dir := newTestStore(t, keyPrefix)
|
|
|
|
insertTestEntry(t, store, dir.Child("a"), 0)
|
|
insertTestEntry(t, store, dir.Child("b"), 0)
|
|
insertLogicallyExpiredTestEntry(t, store, dir.Child("c"))
|
|
insertTestEntry(t, store, dir.Child("d"), 0)
|
|
|
|
if err := store.Client.Del(context.Background(), store.getKey(string(dir.Child("b")))).Err(); err != nil {
|
|
t.Fatalf("drop value key: %v", err)
|
|
}
|
|
|
|
if names := listNames(t, store, dir); fmt.Sprint(names) != "[a d]" {
|
|
t.Fatalf("listed %v, want [a d]", names)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); fmt.Sprint(members) != "[a d]" {
|
|
t.Fatalf("directory index holds %v, want [a d]", members)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListDirectoryEntriesAcrossFetchBatches(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
|
|
var want []string
|
|
for i := 0; i < listFetchBatchSize+2; i++ {
|
|
name := fmt.Sprintf("f%04d", i)
|
|
insertTestEntry(t, store, dir.Child(name), 0)
|
|
if i != listFetchBatchSize {
|
|
want = append(want, name)
|
|
}
|
|
}
|
|
|
|
// the first member of the second batch
|
|
missing := dir.Child(fmt.Sprintf("f%04d", listFetchBatchSize))
|
|
if err := store.Client.Del(context.Background(), store.getKey(string(missing))).Err(); err != nil {
|
|
t.Fatalf("drop value key: %v", err)
|
|
}
|
|
|
|
var names []string
|
|
if _, err := store.ListDirectoryEntries(context.Background(), dir, "", true, 1000, func(entry *filer.Entry) (bool, error) {
|
|
names = append(names, entry.Name())
|
|
return true, nil
|
|
}); err != nil {
|
|
t.Fatalf("ListDirectoryEntries %s: %v", dir, err)
|
|
}
|
|
if fmt.Sprint(names) != fmt.Sprint(want) {
|
|
t.Fatalf("listed %v, want %v", names, want)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); fmt.Sprint(members) != fmt.Sprint(want) {
|
|
t.Fatalf("directory index holds %v, want %v", members, want)
|
|
}
|
|
}
|
|
|
|
func TestListDirectoryEntriesStopsAtUnreadableMember(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
ctx := context.Background()
|
|
|
|
insertTestEntry(t, store, dir.Child("a"), 0)
|
|
insertTestEntry(t, store, dir.Child("b"), 0)
|
|
insertTestEntry(t, store, dir.Child("c"), 0)
|
|
|
|
// a value of the wrong type makes its GET fail with something other than redis.Nil
|
|
key := store.getKey(string(dir.Child("b")))
|
|
if err := store.Client.Del(ctx, key).Err(); err != nil {
|
|
t.Fatalf("drop value key: %v", err)
|
|
}
|
|
if err := store.Client.HSet(ctx, key, "f", "v").Err(); err != nil {
|
|
t.Fatalf("plant hash: %v", err)
|
|
}
|
|
|
|
var names []string
|
|
lastFileName, err := store.ListDirectoryEntries(ctx, dir, "", true, 100, func(entry *filer.Entry) (bool, error) {
|
|
names = append(names, entry.Name())
|
|
return true, nil
|
|
})
|
|
if err == nil {
|
|
t.Fatal("ListDirectoryEntries returned no error for an unreadable member")
|
|
}
|
|
if fmt.Sprint(names) != "[a]" || lastFileName != "b" {
|
|
t.Fatalf("listed %v up to %q, want [a] up to \"b\"", names, lastFileName)
|
|
}
|
|
}
|
|
|
|
func TestDeleteExpiredEntryKeepsRecreatedValue(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
|
|
path := dir.Child("phoenix")
|
|
insertLogicallyExpiredTestEntry(t, store, path)
|
|
// a recreate lands after the lister decided to expire the old value
|
|
insertTestEntry(t, store, path, 0)
|
|
|
|
store.deleteExpiredEntry(context.Background(), dir, path, "phoenix")
|
|
|
|
if names := listNames(t, store, dir); len(names) != 1 || names[0] != "phoenix" {
|
|
t.Fatalf("listed %v, want [phoenix]", names)
|
|
}
|
|
|
|
if members := indexMembers(t, store, dir); len(members) != 1 || members[0] != "phoenix" {
|
|
t.Fatalf("directory index holds %v, want [phoenix]", members)
|
|
}
|
|
}
|
|
|
|
func TestDeleteIfUnchangedScriptOnlyDeletesSameBytes(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
|
|
key := store.getKey(string(dir.Child("guarded")))
|
|
if err := store.Client.Set(context.Background(), key, "v1", 0).Err(); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
|
|
if deleted, err := deleteIfUnchangedScript.Run(context.Background(), store.Client, []string{key}, []byte("v2")).Int(); err != nil || deleted != 0 {
|
|
t.Fatalf("deleted=%d err=%v, want no delete on changed bytes", deleted, err)
|
|
}
|
|
|
|
if deleted, err := deleteIfUnchangedScript.Run(context.Background(), store.Client, []string{key}, []byte("v1")).Int(); err != nil || deleted != 1 {
|
|
t.Fatalf("deleted=%d err=%v, want delete on matching bytes", deleted, err)
|
|
}
|
|
|
|
if exists, err := store.Client.Exists(context.Background(), key).Result(); err != nil || exists != 0 {
|
|
t.Fatalf("exists=%d err=%v, want key gone", exists, err)
|
|
}
|
|
|
|
if deleted, err := deleteIfUnchangedScript.Run(context.Background(), store.Client, []string{key}, []byte("v1")).Int(); err != nil || deleted != -1 {
|
|
t.Fatalf("deleted=%d err=%v, want -1 on a missing key", deleted, err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteEntryKeepsChildListing(t *testing.T) {
|
|
store, dir := newTestStore(t, "")
|
|
ctx := context.Background()
|
|
|
|
insertTestEntry(t, store, dir.Child("obj"), 0)
|
|
|
|
// The listing is the only record that the child sits under this directory, so
|
|
// removing the directory entry must leave it alone. An entry that arrived after
|
|
// the caller judged the directory empty would otherwise become unreachable.
|
|
if err := store.DeleteEntry(ctx, dir); err != nil {
|
|
t.Fatalf("DeleteEntry %s: %v", dir, err)
|
|
}
|
|
if names := listNames(t, store, dir); len(names) != 1 || names[0] != "obj" {
|
|
t.Errorf("child should still be listed after the directory entry is deleted, got %v", names)
|
|
}
|
|
|
|
// DeleteFolderChildren owns that cleanup, and takes the listing with it
|
|
if err := store.DeleteFolderChildren(ctx, dir); err != nil {
|
|
t.Fatalf("DeleteFolderChildren %s: %v", dir, err)
|
|
}
|
|
if names := listNames(t, store, dir); len(names) != 0 {
|
|
t.Errorf("listing should be empty once the children are deleted, got %v", names)
|
|
}
|
|
if n, err := store.Client.Exists(ctx, store.getKey(genDirectoryListKey(string(dir)))).Result(); err != nil {
|
|
t.Fatalf("exists %s: %v", dir, err)
|
|
} else if n != 0 {
|
|
t.Errorf("listing key should be removed with the children, still present")
|
|
}
|
|
}
|