You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
545 lines
14 KiB
545 lines
14 KiB
//go:build foundationdb
|
|
// +build foundationdb
|
|
|
|
package foundationdb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
func TestFoundationDBStore_Initialize(t *testing.T) {
|
|
// Test with default configuration
|
|
config := util.GetViper()
|
|
config.Set("foundationdb.cluster_file", getTestClusterFile())
|
|
config.Set("foundationdb.api_version", 740)
|
|
|
|
store := &FoundationDBStore{}
|
|
err := store.Initialize(config, "foundationdb.")
|
|
if err != nil {
|
|
t.Skip("FoundationDB not available for testing, skipping")
|
|
}
|
|
|
|
defer store.Shutdown()
|
|
|
|
if store.GetName() != "foundationdb" {
|
|
t.Errorf("Expected store name 'foundationdb', got '%s'", store.GetName())
|
|
}
|
|
|
|
if store.directoryPrefix != "seaweedfs" {
|
|
t.Errorf("Expected default directory prefix 'seaweedfs', got '%s'", store.directoryPrefix)
|
|
}
|
|
}
|
|
|
|
func TestFoundationDBStore_InitializeWithCustomConfig(t *testing.T) {
|
|
config := util.GetViper()
|
|
config.Set("foundationdb.cluster_file", getTestClusterFile())
|
|
config.Set("foundationdb.api_version", 740)
|
|
config.Set("foundationdb.timeout", "10s")
|
|
config.Set("foundationdb.max_retry_delay", "2s")
|
|
config.Set("foundationdb.directory_prefix", "custom_prefix")
|
|
|
|
store := &FoundationDBStore{}
|
|
err := store.Initialize(config, "foundationdb.")
|
|
if err != nil {
|
|
t.Skip("FoundationDB not available for testing, skipping")
|
|
}
|
|
|
|
defer store.Shutdown()
|
|
|
|
if store.directoryPrefix != "custom_prefix" {
|
|
t.Errorf("Expected custom directory prefix 'custom_prefix', got '%s'", store.directoryPrefix)
|
|
}
|
|
|
|
if store.timeout != 10*time.Second {
|
|
t.Errorf("Expected timeout 10s, got %v", store.timeout)
|
|
}
|
|
|
|
if store.maxRetryDelay != 2*time.Second {
|
|
t.Errorf("Expected max retry delay 2s, got %v", store.maxRetryDelay)
|
|
}
|
|
}
|
|
|
|
func TestFoundationDBStore_InitializeInvalidConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config map[string]interface{}
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "invalid timeout",
|
|
config: map[string]interface{}{
|
|
"foundationdb.cluster_file": getTestClusterFile(),
|
|
"foundationdb.api_version": 740,
|
|
"foundationdb.timeout": "invalid",
|
|
"foundationdb.directory_prefix": "test",
|
|
},
|
|
errorMsg: "invalid timeout duration",
|
|
},
|
|
{
|
|
name: "invalid max_retry_delay",
|
|
config: map[string]interface{}{
|
|
"foundationdb.cluster_file": getTestClusterFile(),
|
|
"foundationdb.api_version": 740,
|
|
"foundationdb.timeout": "5s",
|
|
"foundationdb.max_retry_delay": "invalid",
|
|
"foundationdb.directory_prefix": "test",
|
|
},
|
|
errorMsg: "invalid max_retry_delay duration",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := util.GetViper()
|
|
for key, value := range tt.config {
|
|
config.Set(key, value)
|
|
}
|
|
|
|
store := &FoundationDBStore{}
|
|
err := store.Initialize(config, "foundationdb.")
|
|
if err == nil {
|
|
store.Shutdown()
|
|
t.Errorf("Expected initialization to fail, but it succeeded")
|
|
} else if !containsString(err.Error(), tt.errorMsg) {
|
|
t.Errorf("Expected error message to contain '%s', got '%s'", tt.errorMsg, err.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFoundationDBStore_KeyGeneration(t *testing.T) {
|
|
store := &FoundationDBStore{}
|
|
err := store.initialize(getTestClusterFile(), 740)
|
|
if err != nil {
|
|
t.Skip("FoundationDB not available for testing, skipping")
|
|
}
|
|
defer store.Shutdown()
|
|
|
|
// Test key generation for different paths
|
|
testCases := []struct {
|
|
dirPath string
|
|
fileName string
|
|
desc string
|
|
}{
|
|
{"/", "file.txt", "root directory file"},
|
|
{"/dir", "file.txt", "subdirectory file"},
|
|
{"/deep/nested/dir", "file.txt", "deep nested file"},
|
|
{"/dir with spaces", "file with spaces.txt", "paths with spaces"},
|
|
{"/unicode/测试", "文件.txt", "unicode paths"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
key := store.genKey(tc.dirPath, tc.fileName)
|
|
if len(key) == 0 {
|
|
t.Error("Generated key should not be empty")
|
|
}
|
|
|
|
// Test that we can extract filename back
|
|
// Note: This tests internal consistency
|
|
if tc.fileName != "" {
|
|
extractedName, err := store.extractFileName(key)
|
|
if err != nil {
|
|
t.Errorf("extractFileName failed: %v", err)
|
|
}
|
|
if extractedName != tc.fileName {
|
|
t.Errorf("Expected extracted filename '%s', got '%s'", tc.fileName, extractedName)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFoundationDBStore_ErrorHandling(t *testing.T) {
|
|
store := &FoundationDBStore{}
|
|
err := store.initialize(getTestClusterFile(), 740)
|
|
if err != nil {
|
|
t.Skip("FoundationDB not available for testing, skipping")
|
|
}
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test FindEntry with non-existent path
|
|
_, err = store.FindEntry(ctx, "/non/existent/file.txt")
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent file")
|
|
}
|
|
if !errors.Is(err, filer_pb.ErrNotFound) {
|
|
t.Errorf("Expected ErrNotFound, got %v", err)
|
|
}
|
|
|
|
// Test KvGet with non-existent key
|
|
_, err = store.KvGet(ctx, []byte("non_existent_key"))
|
|
if err == nil {
|
|
t.Error("Expected error for non-existent key")
|
|
}
|
|
if !errors.Is(err, filer.ErrKvNotFound) {
|
|
t.Errorf("Expected ErrKvNotFound, got %v", err)
|
|
}
|
|
|
|
// Test transaction state errors
|
|
err = store.CommitTransaction(ctx)
|
|
if err == nil {
|
|
t.Error("Expected error when committing without active transaction")
|
|
}
|
|
|
|
err = store.RollbackTransaction(ctx)
|
|
if err == nil {
|
|
t.Error("Expected error when rolling back without active transaction")
|
|
}
|
|
}
|
|
|
|
func TestFoundationDBStore_TransactionState(t *testing.T) {
|
|
store := &FoundationDBStore{}
|
|
err := store.initialize(getTestClusterFile(), 740)
|
|
if err != nil {
|
|
t.Skip("FoundationDB not available for testing, skipping")
|
|
}
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test double transaction begin
|
|
txCtx, err := store.BeginTransaction(ctx)
|
|
if err != nil {
|
|
t.Fatalf("BeginTransaction failed: %v", err)
|
|
}
|
|
|
|
// Try to begin another transaction on the same context
|
|
_, err = store.BeginTransaction(txCtx)
|
|
if err == nil {
|
|
t.Error("Expected error when beginning transaction while one is active")
|
|
}
|
|
|
|
// Commit the transaction
|
|
err = store.CommitTransaction(txCtx)
|
|
if err != nil {
|
|
t.Fatalf("CommitTransaction failed: %v", err)
|
|
}
|
|
|
|
// Now should be able to begin a new transaction
|
|
txCtx2, err := store.BeginTransaction(ctx)
|
|
if err != nil {
|
|
t.Fatalf("BeginTransaction after commit failed: %v", err)
|
|
}
|
|
|
|
// Rollback this time
|
|
err = store.RollbackTransaction(txCtx2)
|
|
if err != nil {
|
|
t.Fatalf("RollbackTransaction failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Benchmark tests
|
|
func BenchmarkFoundationDBStore_InsertEntry(b *testing.B) {
|
|
store := createBenchmarkStore(b)
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
entry := &filer.Entry{
|
|
FullPath: "/benchmark/file.txt",
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
entry.FullPath = util.NewFullPath("/benchmark", fmt.Sprintf("%x", uint64(i))+".txt")
|
|
err := store.InsertEntry(ctx, entry)
|
|
if err != nil {
|
|
b.Fatalf("InsertEntry failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkFoundationDBStore_FindEntry(b *testing.B) {
|
|
store := createBenchmarkStore(b)
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Pre-populate with test entries
|
|
numEntries := 1000
|
|
for i := 0; i < numEntries; i++ {
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath("/benchmark", fmt.Sprintf("%x", uint64(i))+".txt"),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
err := store.InsertEntry(ctx, entry)
|
|
if err != nil {
|
|
b.Fatalf("Pre-population InsertEntry failed: %v", err)
|
|
}
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
path := util.NewFullPath("/benchmark", fmt.Sprintf("%x", uint64(i%numEntries))+".txt")
|
|
_, err := store.FindEntry(ctx, path)
|
|
if err != nil {
|
|
b.Fatalf("FindEntry failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func BenchmarkFoundationDBStore_KvOperations(b *testing.B) {
|
|
store := createBenchmarkStore(b)
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
key := []byte("benchmark_key")
|
|
value := []byte("benchmark_value")
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Put
|
|
err := store.KvPut(ctx, key, value)
|
|
if err != nil {
|
|
b.Fatalf("KvPut failed: %v", err)
|
|
}
|
|
|
|
// Get
|
|
_, err = store.KvGet(ctx, key)
|
|
if err != nil {
|
|
b.Fatalf("KvGet failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
func getTestClusterFile() string {
|
|
clusterFile := os.Getenv("FDB_CLUSTER_FILE")
|
|
if clusterFile == "" {
|
|
clusterFile = "/var/fdb/config/fdb.cluster"
|
|
}
|
|
return clusterFile
|
|
}
|
|
|
|
func createBenchmarkStore(b *testing.B) *FoundationDBStore {
|
|
clusterFile := getTestClusterFile()
|
|
if _, err := os.Stat(clusterFile); os.IsNotExist(err) {
|
|
b.Skip("FoundationDB cluster file not found, skipping benchmark")
|
|
}
|
|
|
|
store := &FoundationDBStore{}
|
|
err := store.initialize(clusterFile, 740)
|
|
if err != nil {
|
|
b.Skipf("Failed to initialize FoundationDB store: %v", err)
|
|
}
|
|
|
|
return store
|
|
}
|
|
|
|
func getTestStore(t *testing.T) *FoundationDBStore {
|
|
t.Helper()
|
|
|
|
clusterFile := getTestClusterFile()
|
|
if _, err := os.Stat(clusterFile); os.IsNotExist(err) {
|
|
t.Skip("FoundationDB cluster file not found, skipping test")
|
|
}
|
|
|
|
store := &FoundationDBStore{}
|
|
if err := store.initialize(clusterFile, 740); err != nil {
|
|
t.Skipf("Failed to initialize FoundationDB store: %v", err)
|
|
}
|
|
|
|
return store
|
|
}
|
|
|
|
func containsString(s, substr string) bool {
|
|
return strings.Contains(s, substr)
|
|
}
|
|
|
|
func TestFoundationDBStore_DeleteFolderChildrenWithBatching(t *testing.T) {
|
|
// This test validates that DeleteFolderChildren always uses batching
|
|
// to safely handle large directories, regardless of transaction context
|
|
|
|
store := getTestStore(t)
|
|
defer store.Shutdown()
|
|
|
|
ctx := context.Background()
|
|
testDir := util.FullPath(fmt.Sprintf("/test_batch_delete_%d", time.Now().UnixNano()))
|
|
|
|
// Create a large directory (> 100 entries to trigger batching)
|
|
const NUM_ENTRIES = 250
|
|
|
|
t.Logf("Creating %d test entries...", NUM_ENTRIES)
|
|
for i := 0; i < NUM_ENTRIES; i++ {
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir), fmt.Sprintf("file_%04d.txt", i)),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
if err := store.InsertEntry(ctx, entry); err != nil {
|
|
t.Fatalf("Failed to insert test entry %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
// Test 1: DeleteFolderChildren outside transaction should succeed
|
|
t.Run("OutsideTransaction", func(t *testing.T) {
|
|
testDir1 := util.FullPath(fmt.Sprintf("/test_batch_1_%d", time.Now().UnixNano()))
|
|
|
|
// Create entries
|
|
for i := 0; i < NUM_ENTRIES; i++ {
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir1), fmt.Sprintf("file_%04d.txt", i)),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
store.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
// Delete with batching
|
|
err := store.DeleteFolderChildren(ctx, testDir1)
|
|
if err != nil {
|
|
t.Errorf("DeleteFolderChildren outside transaction should succeed, got error: %v", err)
|
|
}
|
|
|
|
// Verify all entries deleted
|
|
var count int
|
|
store.ListDirectoryEntries(ctx, testDir1, "", true, 1000, func(entry *filer.Entry) bool {
|
|
count++
|
|
return true
|
|
})
|
|
if count != 0 {
|
|
t.Errorf("Expected all entries to be deleted, found %d", count)
|
|
}
|
|
})
|
|
|
|
// Test 2: DeleteFolderChildren with transaction context - uses its own batched transactions
|
|
t.Run("WithTransactionContext", func(t *testing.T) {
|
|
testDir2 := util.FullPath(fmt.Sprintf("/test_batch_2_%d", time.Now().UnixNano()))
|
|
|
|
// Create entries
|
|
for i := 0; i < NUM_ENTRIES; i++ {
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir2), fmt.Sprintf("file_%04d.txt", i)),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
store.InsertEntry(ctx, entry)
|
|
}
|
|
|
|
// Start a transaction (DeleteFolderChildren will ignore it and use its own batching)
|
|
txCtx, err := store.BeginTransaction(ctx)
|
|
if err != nil {
|
|
t.Fatalf("BeginTransaction failed: %v", err)
|
|
}
|
|
|
|
// Delete large directory - should succeed with batching
|
|
err = store.DeleteFolderChildren(txCtx, testDir2)
|
|
if err != nil {
|
|
t.Errorf("DeleteFolderChildren should succeed with batching even when transaction context present, got: %v", err)
|
|
}
|
|
|
|
// Rollback transaction (DeleteFolderChildren used its own transactions, so this doesn't affect deletions)
|
|
store.RollbackTransaction(txCtx)
|
|
|
|
// Verify entries are still deleted (because DeleteFolderChildren managed its own transactions)
|
|
var count int
|
|
store.ListDirectoryEntries(ctx, testDir2, "", true, 1000, func(entry *filer.Entry) bool {
|
|
count++
|
|
return true
|
|
})
|
|
|
|
if count != 0 {
|
|
t.Errorf("Expected all entries to be deleted, found %d (DeleteFolderChildren uses its own transactions)", count)
|
|
}
|
|
})
|
|
|
|
// Test 3: Nested directories with batching
|
|
t.Run("NestedDirectories", func(t *testing.T) {
|
|
testDir3 := util.FullPath(fmt.Sprintf("/test_batch_3_%d", time.Now().UnixNano()))
|
|
|
|
// Create nested structure
|
|
for i := 0; i < 50; i++ {
|
|
// Files in root
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir3), fmt.Sprintf("file_%02d.txt", i)),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
store.InsertEntry(ctx, entry)
|
|
|
|
// Subdirectory
|
|
subDir := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir3), fmt.Sprintf("dir_%02d", i)),
|
|
Attr: filer.Attr{
|
|
Mode: 0755 | os.ModeDir,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
store.InsertEntry(ctx, subDir)
|
|
|
|
// Files in subdirectory
|
|
for j := 0; j < 3; j++ {
|
|
subEntry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(testDir3)+"/"+fmt.Sprintf("dir_%02d", i), fmt.Sprintf("subfile_%02d.txt", j)),
|
|
Attr: filer.Attr{
|
|
Mode: 0644,
|
|
Uid: 1000,
|
|
Gid: 1000,
|
|
Mtime: time.Now(),
|
|
},
|
|
}
|
|
store.InsertEntry(ctx, subEntry)
|
|
}
|
|
}
|
|
|
|
// Delete all with batching
|
|
err := store.DeleteFolderChildren(ctx, testDir3)
|
|
if err != nil {
|
|
t.Errorf("DeleteFolderChildren should handle nested directories, got: %v", err)
|
|
}
|
|
|
|
// Verify all deleted
|
|
var count int
|
|
store.ListDirectoryEntries(ctx, testDir3, "", true, 1000, func(entry *filer.Entry) bool {
|
|
count++
|
|
return true
|
|
})
|
|
if count != 0 {
|
|
t.Errorf("Expected all nested entries to be deleted, found %d", count)
|
|
}
|
|
})
|
|
|
|
// Cleanup
|
|
store.DeleteFolderChildren(ctx, testDir)
|
|
}
|