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.
		
		
		
		
		
			
		
			
				
					
					
						
							256 lines
						
					
					
						
							7.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							256 lines
						
					
					
						
							7.0 KiB
						
					
					
				| package storage | |
| 
 | |
| import ( | |
| 	"os" | |
| 	"path/filepath" | |
| 	"strconv" | |
| 	"testing" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb" | |
| 	"github.com/seaweedfs/seaweedfs/weed/storage/needle" | |
| 	"github.com/seaweedfs/seaweedfs/weed/storage/super_block" | |
| 	"github.com/seaweedfs/seaweedfs/weed/storage/types" | |
| 	"github.com/seaweedfs/seaweedfs/weed/util" | |
| ) | |
| 
 | |
| // newTestStore creates a test store with the specified number of directories | |
| func newTestStore(t *testing.T, numDirs int) *Store { | |
| 	tempDir := t.TempDir() | |
| 
 | |
| 	var dirs []string | |
| 	var maxCounts []int32 | |
| 	var minFreeSpaces []util.MinFreeSpace | |
| 	var diskTypes []types.DiskType | |
| 
 | |
| 	for i := 0; i < numDirs; i++ { | |
| 		dir := filepath.Join(tempDir, "dir"+strconv.Itoa(i)) | |
| 		os.MkdirAll(dir, 0755) | |
| 		dirs = append(dirs, dir) | |
| 		maxCounts = append(maxCounts, 100) // high limit | |
| 		minFreeSpaces = append(minFreeSpaces, util.MinFreeSpace{}) | |
| 		diskTypes = append(diskTypes, types.HardDriveType) | |
| 	} | |
| 
 | |
| 	store := NewStore(nil, "localhost", 8080, 18080, "http://localhost:8080", | |
| 		dirs, maxCounts, minFreeSpaces, "", NeedleMapInMemory, diskTypes, 3) | |
| 
 | |
| 	// Consume channel messages to prevent blocking | |
| 	done := make(chan bool) | |
| 	go func() { | |
| 		for { | |
| 			select { | |
| 			case <-store.NewVolumesChan: | |
| 			case <-done: | |
| 				return | |
| 			} | |
| 		} | |
| 	}() | |
| 	t.Cleanup(func() { close(done) }) | |
| 
 | |
| 	return store | |
| } | |
| 
 | |
| func TestLocalVolumesLen(t *testing.T) { | |
| 	testCases := []struct { | |
| 		name               string | |
| 		totalVolumes       int | |
| 		remoteVolumes      int | |
| 		expectedLocalCount int | |
| 	}{ | |
| 		{ | |
| 			name:               "all local volumes", | |
| 			totalVolumes:       5, | |
| 			remoteVolumes:      0, | |
| 			expectedLocalCount: 5, | |
| 		}, | |
| 		{ | |
| 			name:               "all remote volumes", | |
| 			totalVolumes:       5, | |
| 			remoteVolumes:      5, | |
| 			expectedLocalCount: 0, | |
| 		}, | |
| 		{ | |
| 			name:               "mixed local and remote", | |
| 			totalVolumes:       10, | |
| 			remoteVolumes:      3, | |
| 			expectedLocalCount: 7, | |
| 		}, | |
| 		{ | |
| 			name:               "no volumes", | |
| 			totalVolumes:       0, | |
| 			remoteVolumes:      0, | |
| 			expectedLocalCount: 0, | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			diskLocation := &DiskLocation{ | |
| 				volumes: make(map[needle.VolumeId]*Volume), | |
| 			} | |
| 
 | |
| 			// Add volumes | |
| 			for i := 0; i < tc.totalVolumes; i++ { | |
| 				vol := &Volume{ | |
| 					Id:         needle.VolumeId(i + 1), | |
| 					volumeInfo: &volume_server_pb.VolumeInfo{}, | |
| 				} | |
| 
 | |
| 				// Mark some as remote | |
| 				if i < tc.remoteVolumes { | |
| 					vol.hasRemoteFile = true | |
| 					vol.volumeInfo.Files = []*volume_server_pb.RemoteFile{ | |
| 						{BackendType: "s3", BackendId: "test", Key: "test-key"}, | |
| 					} | |
| 				} | |
| 
 | |
| 				diskLocation.volumes[vol.Id] = vol | |
| 			} | |
| 
 | |
| 			result := diskLocation.LocalVolumesLen() | |
| 
 | |
| 			if result != tc.expectedLocalCount { | |
| 				t.Errorf("Expected LocalVolumesLen() = %d; got %d (total: %d, remote: %d)", | |
| 					tc.expectedLocalCount, result, tc.totalVolumes, tc.remoteVolumes) | |
| 			} | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| func TestVolumeLoadBalancing(t *testing.T) { | |
| 	testCases := []struct { | |
| 		name              string | |
| 		locations         []locationSetup | |
| 		expectedLocations []int // which location index should get each volume | |
| 	}{ | |
| 		{ | |
| 			name: "even distribution across empty locations", | |
| 			locations: []locationSetup{ | |
| 				{localVolumes: 0, remoteVolumes: 0}, | |
| 				{localVolumes: 0, remoteVolumes: 0}, | |
| 				{localVolumes: 0, remoteVolumes: 0}, | |
| 			}, | |
| 			expectedLocations: []int{0, 1, 2, 0, 1, 2}, // round-robin | |
| 		}, | |
| 		{ | |
| 			name: "prefers location with fewer local volumes", | |
| 			locations: []locationSetup{ | |
| 				{localVolumes: 5, remoteVolumes: 0}, | |
| 				{localVolumes: 2, remoteVolumes: 0}, | |
| 				{localVolumes: 8, remoteVolumes: 0}, | |
| 			}, | |
| 			expectedLocations: []int{1, 1, 1}, // all go to location 1 (has fewest) | |
| 		}, | |
| 		{ | |
| 			name: "ignores remote volumes in count", | |
| 			locations: []locationSetup{ | |
| 				{localVolumes: 2, remoteVolumes: 10}, // 2 local, 10 remote | |
| 				{localVolumes: 5, remoteVolumes: 0},  // 5 local | |
| 				{localVolumes: 3, remoteVolumes: 0},  // 3 local | |
| 			}, | |
| 			// expectedLocations: []int{0, 0, 2} | |
| 			// Explanation: | |
| 			// 1. Initial local counts: [2, 5, 3]. First volume goes to location 0 (2 local, ignoring 10 remote). | |
| 			// 2. New local counts: [3, 5, 3]. Second volume goes to location 0 (first with min count 3). | |
| 			// 3. New local counts: [4, 5, 3]. Third volume goes to location 2 (3 local < 4 local). | |
| 			expectedLocations: []int{0, 0, 2}, | |
| 		}, | |
| 		{ | |
| 			name: "balances when some locations have remote volumes", | |
| 			locations: []locationSetup{ | |
| 				{localVolumes: 1, remoteVolumes: 5}, | |
| 				{localVolumes: 1, remoteVolumes: 0}, | |
| 				{localVolumes: 0, remoteVolumes: 3}, | |
| 			}, | |
| 			// expectedLocations: []int{2, 0, 1} | |
| 			// Explanation: | |
| 			// 1. Initial local counts: [1, 1, 0]. First volume goes to location 2 (0 local). | |
| 			// 2. New local counts: [1, 1, 1]. Second volume goes to location 0 (first with min count 1). | |
| 			// 3. New local counts: [2, 1, 1]. Third volume goes to location 1 (next with min count 1). | |
| 			expectedLocations: []int{2, 0, 1}, | |
| 		}, | |
| 	} | |
| 
 | |
| 	for _, tc := range testCases { | |
| 		t.Run(tc.name, func(t *testing.T) { | |
| 			// Create test store with multiple directories | |
| 			store := newTestStore(t, len(tc.locations)) | |
| 
 | |
| 			// Pre-populate locations with volumes | |
| 			for locIdx, setup := range tc.locations { | |
| 				location := store.Locations[locIdx] | |
| 				vidCounter := 1000 + locIdx*100 // unique volume IDs per location | |
|  | |
| 				// Add local volumes | |
| 				for i := 0; i < setup.localVolumes; i++ { | |
| 					vol := createTestVolume(needle.VolumeId(vidCounter), false) | |
| 					location.SetVolume(vol.Id, vol) | |
| 					vidCounter++ | |
| 				} | |
| 
 | |
| 				// Add remote volumes | |
| 				for i := 0; i < setup.remoteVolumes; i++ { | |
| 					vol := createTestVolume(needle.VolumeId(vidCounter), true) | |
| 					location.SetVolume(vol.Id, vol) | |
| 					vidCounter++ | |
| 				} | |
| 			} | |
| 
 | |
| 			// Create volumes and verify they go to expected locations | |
| 			for i, expectedLoc := range tc.expectedLocations { | |
| 				volumeId := needle.VolumeId(i + 1) | |
| 
 | |
| 				err := store.AddVolume(volumeId, "", NeedleMapInMemory, "000", "", | |
| 					0, needle.GetCurrentVersion(), 0, types.HardDriveType, 3) | |
| 
 | |
| 				if err != nil { | |
| 					t.Fatalf("Failed to add volume %d: %v", volumeId, err) | |
| 				} | |
| 
 | |
| 				// Find which location got the volume | |
| 				actualLoc := -1 | |
| 				for locIdx, location := range store.Locations { | |
| 					if _, found := location.FindVolume(volumeId); found { | |
| 						actualLoc = locIdx | |
| 						break | |
| 					} | |
| 				} | |
| 
 | |
| 				if actualLoc != expectedLoc { | |
| 					t.Errorf("Volume %d: expected location %d, got location %d", | |
| 						volumeId, expectedLoc, actualLoc) | |
| 
 | |
| 					// Debug info | |
| 					for locIdx, loc := range store.Locations { | |
| 						localCount := loc.LocalVolumesLen() | |
| 						totalCount := loc.VolumesLen() | |
| 						t.Logf("  Location %d: %d local, %d total", locIdx, localCount, totalCount) | |
| 					} | |
| 				} | |
| 			} | |
| 		}) | |
| 	} | |
| } | |
| 
 | |
| // Helper types and functions | |
| type locationSetup struct { | |
| 	localVolumes  int | |
| 	remoteVolumes int | |
| } | |
| 
 | |
| func createTestVolume(vid needle.VolumeId, isRemote bool) *Volume { | |
| 	vol := &Volume{ | |
| 		Id:         vid, | |
| 		SuperBlock: super_block.SuperBlock{}, | |
| 		volumeInfo: &volume_server_pb.VolumeInfo{}, | |
| 	} | |
| 
 | |
| 	if isRemote { | |
| 		vol.hasRemoteFile = true | |
| 		vol.volumeInfo.Files = []*volume_server_pb.RemoteFile{ | |
| 			{BackendType: "s3", BackendId: "test", Key: "remote-key-" + strconv.Itoa(int(vid))}, | |
| 		} | |
| 	} | |
| 
 | |
| 	return vol | |
| }
 |