Files
seaweedfs/weed/filer/redis3/universal_redis_store_test.go
T
Chris LuandDevin cb993ab83b filer.redis3: accept username and keyPrefix options (#12192)
* filer.redis3: prefix every key via getKey, honoring keyPrefix

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer.redis3: read username and keyPrefix options into client config

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer.redis3: reject a username configured without a password

go-redis only sends AUTH when a password is set, so a configured username
with an empty password silently connects as the default user instead of
the intended ACL user. Fail loudly at initialize instead.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer.redis3: cover keyPrefix scoping in the store tests

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* filer.redis3: derive the test prefix and kv key from the per-run directory

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-28 22:54:07 -07:00

197 lines
6.6 KiB
Go

package redis3
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/go-redsync/redsync/v4"
goredis "github.com/go-redsync/redsync/v4/redis/goredis/v9"
"github.com/redis/go-redis/v9"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func newTestStore(t *testing.T) (*UniversalRedis3Store, util.FullPath) {
t.Helper()
if os.Getenv("RUN_REDIS_TESTS") != "1" {
t.Skip("redis3 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 := &UniversalRedis3Store{Client: client, redsync: redsync.New(goredis.NewPool(client))}
dir := util.FullPath(fmt.Sprintf("/redis3_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 listNames(t *testing.T, store *UniversalRedis3Store, dir util.FullPath) []string {
t.Helper()
names := []string{}
if _, err := store.ListDirectoryEntries(context.Background(), dir, "", true, 100, func(entry *filer.Entry) (bool, error) {
names = append(names, entry.Name())
return true, nil
}); err != nil {
t.Fatalf("list %s: %v", dir, err)
}
return names
}
func TestDeleteEntryKeepsChildListing(t *testing.T) {
store, dir := newTestStore(t)
ctx := context.Background()
now := time.Now()
child := dir.Child("obj")
if err := store.InsertEntry(ctx, &filer.Entry{
FullPath: child,
Attr: filer.Attr{Crtime: now, Mtime: now, Mode: 0644},
}); err != nil {
t.Fatalf("InsertEntry %s: %v", child, err)
}
// 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, 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")
}
}
func TestRemovingTheLastChildDropsTheListing(t *testing.T) {
store, dir := newTestStore(t)
ctx := context.Background()
now := time.Now()
child := dir.Child("obj")
if err := store.InsertEntry(ctx, &filer.Entry{
FullPath: child,
Attr: filer.Attr{Crtime: now, Mtime: now, Mode: 0644},
}); err != nil {
t.Fatalf("InsertEntry %s: %v", child, err)
}
// An emptied listing must not leave its header behind: cleanup deletes such a
// folder without touching the listing, so the key would never be collected.
if err := store.DeleteEntry(ctx, child); err != nil {
t.Fatalf("DeleteEntry %s: %v", child, err)
}
if n, err := store.Client.Exists(ctx, genDirectoryListKey(string(dir))).Result(); err != nil {
t.Fatalf("exists %s: %v", dir, err)
} else if n != 0 {
t.Errorf("listing key should be gone once the last child is removed, still present")
}
}
// A configured keyPrefix must scope every key the store writes: entry values,
// directory listings (and their skiplist nodes and locks), and Kv keys. The
// raw-key assertions below would still pass a round-trip check if reads and
// writes both dropped the prefix, so they check the actual key space.
func TestKeyPrefixScopesAllKeys(t *testing.T) {
store, dir := newTestStore(t)
store.keyPrefix = string(dir) + "_"
kvKey := string(dir) + "_k"
ctx := context.Background()
now := time.Now()
child := dir.Child("obj")
if err := store.InsertEntry(ctx, &filer.Entry{
FullPath: child,
Attr: filer.Attr{Crtime: now, Mtime: now, Mode: 0644},
}); err != nil {
t.Fatalf("InsertEntry %s: %v", child, err)
}
if _, err := store.FindEntry(ctx, child); err != nil {
t.Fatalf("FindEntry %s: %v", child, err)
}
if names := listNames(t, store, dir); len(names) != 1 || names[0] != "obj" {
t.Fatalf("list %s: got %v", dir, names)
}
for _, raw := range []string{string(child), genDirectoryListKey(string(dir))} {
if n, err := store.Client.Exists(ctx, raw).Result(); err != nil || n != 0 {
t.Fatalf("key %q should not exist unprefixed: n=%d err=%v", raw, n, err)
}
if n, err := store.Client.Exists(ctx, store.keyPrefix+raw).Result(); err != nil || n != 1 {
t.Fatalf("key %q should exist under the prefix: n=%d err=%v", raw, n, err)
}
}
if err := store.KvPut(ctx, []byte(kvKey), []byte("v")); err != nil {
t.Fatalf("KvPut: %v", err)
}
if v, err := store.KvGet(ctx, []byte(kvKey)); err != nil || string(v) != "v" {
t.Fatalf("KvGet: v=%q err=%v", v, err)
}
if _, err := store.Client.Get(ctx, kvKey).Result(); err != redis.Nil {
t.Fatalf("kv key should not exist unprefixed: err=%v", err)
}
if v, err := store.Client.Get(ctx, store.keyPrefix+kvKey).Result(); err != nil || v != "v" {
t.Fatalf("kv key should exist under the prefix: v=%q err=%v", v, err)
}
if err := store.KvDelete(ctx, []byte(kvKey)); err != nil {
t.Fatalf("KvDelete: %v", err)
}
if _, err := store.KvGet(ctx, []byte(kvKey)); err != filer.ErrKvNotFound {
t.Fatalf("KvGet after delete: err=%v", err)
}
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.Fatalf("listing should be empty once the children are deleted, got %v", names)
}
if err := store.DeleteEntry(ctx, dir); err != nil {
t.Fatalf("DeleteEntry %s: %v", dir, err)
}
if keys, err := store.Client.Keys(ctx, store.keyPrefix+"*").Result(); err != nil || len(keys) != 0 {
t.Fatalf("no prefixed keys should remain after cleanup: keys=%v err=%v", keys, err)
}
}