perf(clustermgr): alloc first choose volume which disks have more free space

with: #1000910653

Signed-off-by: JasonHu520 <huzongchao@oppo.com>
This commit is contained in:
JasonHu520 2026-04-23 16:18:55 +08:00 committed by slasher
parent c170c6c606
commit f230337013
18 changed files with 1182 additions and 11 deletions

View File

@ -47,6 +47,7 @@ func TestApplier_Others(t *testing.T) {
// test flush
{
testDiskMgr.RegisterDiskUsageCallback(func(proto.DiskID, float64) {})
heartbeatInfos := make([]*clustermgr.DiskHeartBeatInfo, 0)
for i := 1; i <= 10; i++ {
diskInfo, err := testDiskMgr.GetDiskInfo(ctx, proto.DiskID(i))

View File

@ -57,6 +57,8 @@ type BlobNodeManagerAPI interface {
AllocChunks(ctx context.Context, policy AllocPolicy) ([]proto.DiskID, []proto.Vuid, error)
// HasEnoughSpace reports whether the cluster currently has enough space to create
HasEnoughSpace(ctx context.Context, mode codemode.CodeMode) bool
// RegisterDiskUsageCallback registers a callback that is invoked on every disk heartbeat.
RegisterDiskUsageCallback(fn func(diskID proto.DiskID, ratio float64))
NodeManagerAPI
persistentHandler
@ -147,6 +149,12 @@ type BlobNodeManager struct {
nodeDiskTable *normaldb.BlobNodeDiskTable
blobNodeClient blobnode.StorageAPI
diskUsageCallback func(diskID proto.DiskID, ratio float64)
}
func (b *BlobNodeManager) RegisterDiskUsageCallback(fn func(diskID proto.DiskID, ratio float64)) {
b.diskUsageCallback = fn
}
func (b *BlobNodeManager) Start() {
@ -958,6 +966,10 @@ func (b *BlobNodeManager) applyHeartBeatDiskInfo(ctx context.Context, infos []*c
return nil
})
if info.Size > 0 && b.diskUsageCallback != nil {
b.diskUsageCallback(info.DiskID, float64(info.Used)/float64(info.Size))
}
}
return nil
}

View File

@ -227,6 +227,7 @@ func TestDiskMgr_Heartbeat(t *testing.T) {
defer closeTestDiskMgr()
initTestBlobNodeMgrNodes(t, testDiskMgr, 1, 1, testIdcs[0])
initTestBlobNodeMgrDisks(t, testDiskMgr, 1, 10, false, testIdcs[0])
testDiskMgr.RegisterDiskUsageCallback(func(proto.DiskID, float64) {})
_, ctx := trace.StartSpanFromContext(context.Background(), "")
heartbeatInfos := make([]*clustermgr.DiskHeartBeatInfo, 0)
@ -974,6 +975,69 @@ func TestBlobNodeManager_ListNodes(t *testing.T) {
require.NotContains(t, normalIDs, proto.NodeID(1)) // node 1 is Dropped
}
func TestBlobNodeMgr_DiskUsageListener(t *testing.T) {
mgr, closeMgr := initTestBlobNodeMgr(t)
defer closeMgr()
_, ctx := trace.StartSpanFromContext(context.Background(), "")
initTestBlobNodeMgrNodes(t, mgr, 1, 1, testIdcs[0])
initTestBlobNodeMgrDisks(t, mgr, 1, 1, true, testIdcs[0])
const diskID = proto.DiskID(1)
const total = int64(100 * 1024 * 1024 * 1024) // 100 GiB
var lastID proto.DiskID
var lastRatio float64
mgr.RegisterDiskUsageCallback(func(id proto.DiskID, ratio float64) {
lastID = id
lastRatio = ratio
})
const watermark = 0.8
sendHeartbeat := func(used int64) {
infos := []*clustermgr.DiskHeartBeatInfo{{
DiskID: diskID,
Size: total,
Used: used,
}}
require.NoError(t, mgr.applyHeartBeatDiskInfo(ctx, infos))
}
t.Run("unknown disk skips listener", func(t *testing.T) {
lastRatio = 0
require.NoError(t, mgr.applyHeartBeatDiskInfo(ctx, []*clustermgr.DiskHeartBeatInfo{
{DiskID: proto.DiskID(9999), Size: total, Free: total / 2},
}))
require.Equal(t, proto.DiskID(0), lastID)
})
t.Run("zero-size disk skips callback", func(t *testing.T) {
prevID := lastID
require.NoError(t, mgr.applyHeartBeatDiskInfo(ctx, []*clustermgr.DiskHeartBeatInfo{
{DiskID: diskID, Size: 0, Used: 0},
}))
require.Equal(t, prevID, lastID)
})
t.Run("below watermark notifies ratio < threshold", func(t *testing.T) {
sendHeartbeat(79 * 1024 * 1024 * 1024) // 79%
require.Equal(t, diskID, lastID)
require.Less(t, lastRatio, watermark)
})
t.Run("at watermark boundary notifies ratio >= threshold", func(t *testing.T) {
sendHeartbeat(80 * 1024 * 1024 * 1024) // exactly 80%
require.Equal(t, diskID, lastID)
require.GreaterOrEqual(t, lastRatio, watermark)
})
t.Run("above watermark notifies ratio >= threshold", func(t *testing.T) {
sendHeartbeat(95 * 1024 * 1024 * 1024) // 95%
require.Equal(t, diskID, lastID)
require.GreaterOrEqual(t, lastRatio, watermark)
})
}
// TestDiskMgr_DroppingNode covers applyDroppingNode/applyDroppedNode corner cases:
// node not found, dropping propagation, and dropped without disks.
func TestDiskMgr_DroppingNode(t *testing.T) {

View File

@ -198,6 +198,18 @@ func (mr *MockBlobNodeManagerAPIMockRecorder) HasEnoughSpace(arg0, arg1 interfac
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasEnoughSpace", reflect.TypeOf((*MockBlobNodeManagerAPI)(nil).HasEnoughSpace), arg0, arg1)
}
// RegisterDiskUsageCallback mocks base method.
func (m *MockBlobNodeManagerAPI) RegisterDiskUsageCallback(fn func(proto.DiskID, float64)) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "RegisterDiskUsageCallback", fn)
}
// RegisterDiskUsageCallback indicates an expected call of RegisterDiskUsageCallback.
func (mr *MockBlobNodeManagerAPIMockRecorder) RegisterDiskUsageCallback(fn interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterDiskUsageCallback", reflect.TypeOf((*MockBlobNodeManagerAPI)(nil).RegisterDiskUsageCallback), fn)
}
// IsDiskWritable mocks base method.
func (m *MockBlobNodeManagerAPI) IsDiskWritable(arg0 context.Context, arg1 proto.DiskID) (bool, error) {
m.ctrl.T.Helper()

View File

@ -27,6 +27,7 @@ import (
"github.com/cubefs/cubefs/blobstore/clustermgr/base"
"github.com/cubefs/cubefs/blobstore/common/trace"
mock "github.com/cubefs/cubefs/blobstore/testing/mockclustermgr"
"github.com/cubefs/cubefs/blobstore/util/errors"
)
func TestConfigMgr_Others(t *testing.T) {
@ -50,10 +51,20 @@ func TestConfigMgr_Others(t *testing.T) {
module := configmgr.GetModuleName()
require.Equal(t, testModuleName, module)
err = configmgr.LoadData(ctx)
require.NoError(t, err)
configmgr.SetRaftServer(nil)
err = configmgr.Flush(ctx)
require.NoError(t, err)
configmgr.NotifyLeaderChange(ctx, 0, "")
// cover Get path when key is not in DB or default config
mockKvMgr.EXPECT().Get("unknown_key").Return(nil, errors.New("not found"))
_, err = configmgr.Get(ctx, "unknown_key")
require.Error(t, err)
}
func TestConfigMgr_Apply(t *testing.T) {
@ -144,4 +155,28 @@ func TestConfigMgr_Apply(t *testing.T) {
err = configmgr.Apply(ctx, operTypes, datas, ctxs)
require.NoError(t, err)
// OperTypeSetConfig: Set returns error
{
ctr2 := gomock.NewController(t)
mockKvMgr2 := mock.NewMockKvMgrAPI(ctr2)
mockKvMgr2.EXPECT().Set(gomock.Any(), gomock.Any()).Return(errors.New("set error"))
cm2, err := New(mockKvMgr2, cfMap)
require.NoError(t, err)
data, _ := json.Marshal(&clustermgr.ConfigSetArgs{Key: "forbid_sync_config", Value: "true"})
err = cm2.Apply(ctx, []int32{OperTypeSetConfig}, [][]byte{data}, []base.ProposeContext{{ReqID: span.TraceID()}})
require.Error(t, err)
}
// OperTypeDeleteConfig: Delete returns error
{
ctr3 := gomock.NewController(t)
mockKvMgr3 := mock.NewMockKvMgrAPI(ctr3)
mockKvMgr3.EXPECT().Delete(gomock.Any()).Return(errors.New("delete error"))
cm3, err := New(mockKvMgr3, cfMap)
require.NoError(t, err)
data, _ := json.Marshal(&clustermgr.ConfigArgs{Key: "forbid_sync_config"})
err = cm3.Apply(ctx, []int32{OperTypeDeleteConfig}, [][]byte{data}, []base.ProposeContext{{ReqID: span.TraceID()}})
require.Error(t, err)
}
}

View File

@ -43,11 +43,17 @@ func TestConfigMgr(t *testing.T) {
"enable_recycle": false,
"idc": []interface{}{"idc1", "idc2"},
"test_map1": map[string]interface{}{"a": 1, "b": 2},
"region": "cn-south",
}
configMgr, err := New(kvMgr, cfMap)
require.NoError(t, err)
// string default values are stored directly
regionVal, err := configMgr.Get(ctx, "region")
require.NoError(t, err)
require.Equal(t, "cn-south", regionVal)
ret, _ := configMgr.Get(ctx, "enable_recycle")
var enableRec bool
json.Unmarshal([]byte(ret), &enableRec)
@ -67,4 +73,19 @@ func TestConfigMgr(t *testing.T) {
var vv3 map[string]interface{}
json.Unmarshal([]byte(idcRet2), &vv3)
require.Equal(t, map[string]interface{}{"a": float64(1), "b": float64(2)}, vv3)
// cover Get path that returns value directly from DB
err = configMgr.Set(ctx, "db_only_key", "db_value")
require.NoError(t, err)
dbVal, err := configMgr.Get(ctx, "db_only_key")
require.NoError(t, err)
require.Equal(t, "db_value", dbVal)
// cover Get path that returns error for empty key
_, err = configMgr.Get(ctx, "")
require.Error(t, err)
// cover Get path when key is not in DB or default config
_, err = configMgr.Get(ctx, "totally_nonexistent_key")
require.Error(t, err)
}

View File

@ -39,6 +39,7 @@ type allocConfig struct {
allocatableSize uint64
codeModes map[codemode.CodeMode]codeModeConf
shardNum int
diskUsageThreshold float64
}
type idleItem struct {
@ -136,16 +137,23 @@ type activeVolumes struct {
sync.RWMutex
}
// volume allocator, use for allocating volume
type volumeAllocator struct {
// idle volumes
idles map[codemode.CodeMode]*idleVolumes
// actives volumes
actives *activeVolumes
// diskHighUsage holds the latest high-watermark state per disk.
diskHighUsage sync.Map // map[proto.DiskID]bool
allocConfig
}
// UpdateDiskHighUsage is called on every disk heartbeat to record whether a
// disk currently exceeds the high-usage threshold.
func (a *volumeAllocator) UpdateDiskHighUsage(diskID proto.DiskID, ratio float64) {
a.diskHighUsage.Store(diskID, ratio > a.diskUsageThreshold)
}
type sortVid []vidLoad
type vidLoad struct {
@ -240,11 +248,11 @@ func (a *volumeAllocator) Insert(v *volume, mode codemode.CodeMode) {
a.idles[mode].addAllocatable(v)
}
// PreAlloc select volumes which can alloc
// 1. when EnableDiskLoad=false, all volume will range by health, the healthier volume will range in front of the optional head
// 2. when EnableDiskLoad=true, if do not hash enough volumes to alloc ,
// 1. first add disk's load and retry, each time add one until disk's load equal to diskLoadThreshold will set EnableDiskLoad=false
// 2. second minus volume score and retry , each time minus one until volume's score equal to scoreThreshold
// PreAlloc pre-allocates up to count volumes for the given code mode.
// When a.diskChecker is set, volumes with any high-watermark disk are deferred
// to a later retry pass so that volumes on emptier disks are preferred.
// The fallback guarantees service continuity: when no low usage disk candidates
// remain, high disk usage volumes are included automatically.
func (a *volumeAllocator) PreAlloc(ctx context.Context, mode codemode.CodeMode, count int) ([]proto.Vid, int) {
span := trace.SpanFromContextSafe(ctx)
idleVolumes := a.idles[mode]
@ -260,6 +268,9 @@ func (a *volumeAllocator) PreAlloc(ctx context.Context, mode codemode.CodeMode,
scoreThreshold := healthiestScore
// diskLoadThreshold start half of allocatableDiskLoadThreshold,avoid loop too much times
diskLoadThreshold := a.allocatableDiskLoadThreshold / 2
// skipWatermark becomes true after disk-load is fully relaxed, allowing
// volumes on high-watermark disks to be included as a last-resort fallback.
skipDiskUsageCheck := a.diskUsageThreshold <= 0
// optionalVids include all volume id which satisfied with our condition(idle/enough free size/health/not over disk load)
// all vid will range by health, the healthier volume will range in front of the optional head
optionalVids := make([]proto.Vid, 0)
@ -281,7 +292,10 @@ RETRY:
now := time.Now()
for idx, volume := range allIdles {
volume.lock.RLock()
if volume.canAlloc(a.allocatableSize, scoreThreshold) && (!isEnableDiskLoad || !a.isOverload(volume.vUnits, diskLoadThreshold)) {
hasHighUsageDisk := !skipDiskUsageCheck && a.hasHighUsageDisk(volume.vUnits, mode)
if volume.canAlloc(a.allocatableSize, scoreThreshold) &&
(!isEnableDiskLoad || !a.isOverload(volume.vUnits, diskLoadThreshold)) &&
!hasHighUsageDisk {
optionalVids = append(optionalVids, volume.vid)
// only insufficient free size or unhealthy volume move to temporary head,
// ignore over diskLoad volume
@ -296,8 +310,10 @@ RETRY:
break
}
// go to the end, first retry with high disk load volume
// second lower health score volume
// Relaxation order on retry:
// 1. raise diskLoadThreshold step by step
// 2. disable disk-load check entirely and relax disk high usage filter (skipDiskCheck = true)
// 3. lower scoreThreshold
if isLastShard && idx == len(allIdles)-1 {
span.Infof("assignable volume length is %d", len(assignable))
if len(assignable) == 0 {
@ -307,10 +323,14 @@ RETRY:
if isEnableDiskLoad && diskLoadThreshold < a.allocatableDiskLoadThreshold {
// When diskLoad exceeds the threshold, retry 3 times at most
diskLoadThreshold += int(math.Ceil(float64(a.allocatableDiskLoadThreshold) / 6.0))
} else if isEnableDiskLoad {
span.Infof("increase diskload to %d", diskLoadThreshold)
} else if !skipDiskUsageCheck || isEnableDiskLoad {
skipDiskUsageCheck = true
isEnableDiskLoad = false
span.Info("close diskload and disk usage check")
} else if scoreThreshold > allocatableScoreThreshold {
scoreThreshold -= 1
span.Warnf("lower volume health score %d", scoreThreshold)
}
allIdles = assignable
assignable = assignable[:0]
@ -427,6 +447,20 @@ func (a *volumeAllocator) getShardNum(mode codemode.CodeMode) int {
return modeConf.tactic.N + modeConf.tactic.M + modeConf.tactic.L
}
// hasHighUsageDisk reports whether any disk in vUnits exceeds the high usage threshold.
func (a *volumeAllocator) hasHighUsageDisk(vUnits []*volumeUnit, mode codemode.CodeMode) bool {
count := mode.GetShardNum() - mode.T().PutQuorum
for _, unit := range vUnits {
if v, ok := a.diskHighUsage.Load(unit.vuInfo.DiskID); ok && v.(bool) {
count = count - 1
if count < 0 {
return true
}
}
}
return false
}
func (a *volumeAllocator) sortVidByHealthAndDiskLoad(mode codemode.CodeMode, vids []proto.Vid) (ret []proto.Vid) {
if len(vids) <= 1 {
return vids

View File

@ -54,6 +54,8 @@ type VolumeMgrConfig struct {
MinWritableVolumeSpace uint64 `json:"min_writable_volume_space"`
AllocatableDiskLoadThreshold int `json:"allocatable_disk_load_threshold"`
AllocFactor int `json:"alloc_factor"`
AllocDiskUsageThreshold float64 `json:"alloc_disk_usage_threshold"`
// the volume free size must big than AllocatableSize can alloc
AllocatableSize uint64 `json:"allocatable_size"`
// the number of volume partitions that can be allocated
@ -171,8 +173,11 @@ func NewVolumeMgr(conf VolumeMgrConfig, diskMgr cluster.BlobNodeManagerAPI, scop
allocFactor: conf.AllocFactor,
allocatableDiskLoadThreshold: conf.AllocatableDiskLoadThreshold,
shardNum: conf.ShardNum,
diskUsageThreshold: conf.AllocDiskUsageThreshold,
}
volAllocator := newVolumeAllocator(allocConfig)
diskMgr.RegisterDiskUsageCallback(volAllocator.UpdateDiskHighUsage)
volumeMgr.allocator = volAllocator
// initial register change status callback func

View File

@ -103,6 +103,7 @@ func initMockVolumeMgr(t testing.TB) (*VolumeMgr, func()) {
mockDiskMgr.EXPECT().Stat(gomock.Any(), proto.DiskTypeHDD).AnyTimes().Return(&clustermgr.SpaceStatInfo{TotalDisk: 35})
mockDiskMgr.EXPECT().IsDiskWritable(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(mockIsDiskWritable)
mockDiskMgr.EXPECT().GetDiskInfo(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(mockGetDiskInfo)
mockDiskMgr.EXPECT().RegisterDiskUsageCallback(gomock.Any()).Times(1)
mockVolumeMgr, err := NewVolumeMgr(testConfig, mockDiskMgr, mockScopeMgr, mockConfigMgr, volumeDB)
require.NoError(t, err)
@ -317,6 +318,7 @@ func Test_NewVolumeMgr(t *testing.T) {
mockDiskMgr.EXPECT().IsDiskWritable(gomock.Any(), gomock.Any()).AnyTimes().Return(true, nil)
mockDiskMgr.EXPECT().GetDiskInfo(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(mockGetDiskInfo)
mockDiskMgr.EXPECT().HasEnoughSpace(gomock.Any(), gomock.Any()).AnyTimes().Return(true)
mockDiskMgr.EXPECT().RegisterDiskUsageCallback(gomock.Any()).Times(1)
mockVolumeMgr, err := NewVolumeMgr(volConfig, mockDiskMgr, mockScopeMgr, mockConfigMgr, volumeDB)
require.NoError(t, err)
@ -1201,6 +1203,166 @@ func BenchmarkVolumeMgr_AllocVolume(b *testing.B) {
})
}
// TestPreAlloc_HighWatermarkFallback verifies the two-pass watermark behavior:
// 1. When allocating 2 volumes from a pool of 4 high-watermark + 2 low-watermark
// volumes (all other conditions equal), the 2 low-watermark volumes are returned.
// 2. When allocating all 6, the full set is returned via graceful fallback.
func TestPreAlloc_HighWatermarkFallback(t *testing.T) {
testConfig.checkAndFix()
codeModes := make(map[codemode.CodeMode]codeModeConf)
for _, policy := range testConfig.CodeModePolicies {
cm := policy.ModeName.GetCodeMode()
codeModes[cm] = codeModeConf{
mode: cm,
sizeRatio: policy.SizeRatio,
tactic: cm.Tactic(),
enable: policy.Enable,
}
}
mode := codemode.EC15P12
newAllocator := func() *volumeAllocator {
a := newVolumeAllocator(allocConfig{
codeModes: codeModes,
allocatableSize: testConfig.AllocatableSize,
allocFactor: testConfig.AllocFactor,
allocatableDiskLoadThreshold: testConfig.AllocatableDiskLoadThreshold,
shardNum: testConfig.ShardNum,
diskUsageThreshold: 0.85,
})
// vid 1..4: high-watermark disks (ID <= 1000); vid 5..6: low-watermark disks (ID > 1000).
const watermarkBoundary = proto.DiskID(1000)
allVols := generateVolume(mode, 6, 1)
for i, vol := range allVols {
for j := range vol.vUnits {
if i < 4 {
vol.vUnits[j].vuInfo.DiskID = proto.DiskID(j + 1)
} else {
vol.vUnits[j].vuInfo.DiskID = watermarkBoundary + proto.DiskID(j+1)
}
}
vol.volInfoBase.Status = proto.VolumeStatusIdle
a.idles[mode].addAllocatable(vol)
}
// mark disks with ID <= watermarkBoundary as high-usage via the heartbeat-driven path.
for _, vol := range allVols {
for _, unit := range vol.vUnits {
var ratio float64
if unit.vuInfo.DiskID <= watermarkBoundary {
ratio = 0.9 // above threshold
} else {
ratio = 0.5 // below threshold
}
a.UpdateDiskHighUsage(unit.vuInfo.DiskID, ratio)
}
}
return a
}
_, ctx := trace.StartSpanFromContext(context.Background(), "")
lowWatermarkVids := map[proto.Vid]struct{}{5: {}, 6: {}}
t.Run("prefer low-watermark volumes", func(t *testing.T) {
ret, _ := newAllocator().PreAlloc(ctx, mode, 2)
require.Equal(t, 2, len(ret))
for _, vid := range ret {
require.Contains(t, lowWatermarkVids, vid, "expected only low-watermark vids, got vid=%d", vid)
}
})
t.Run("fallback allocates all volumes", func(t *testing.T) {
ret, _ := newAllocator().PreAlloc(ctx, mode, 6)
require.Equal(t, 6, len(ret))
})
t.Run("disabled when threshold is zero", func(t *testing.T) {
// diskUsageThreshold=0 → skipDiskUsageCheck=true from the start;
// even disks at 90% usage are eligible without fallback.
a := newVolumeAllocator(allocConfig{
codeModes: codeModes,
allocatableSize: testConfig.AllocatableSize,
allocFactor: testConfig.AllocFactor,
allocatableDiskLoadThreshold: testConfig.AllocatableDiskLoadThreshold,
shardNum: testConfig.ShardNum,
diskUsageThreshold: 0,
})
vols := generateVolume(mode, 4, 1)
for _, vol := range vols {
vol.volInfoBase.Status = proto.VolumeStatusIdle
a.idles[mode].addAllocatable(vol)
for _, unit := range vol.vUnits {
a.UpdateDiskHighUsage(unit.vuInfo.DiskID, 0.9)
}
}
ret, _ := a.PreAlloc(ctx, mode, 2)
require.Equal(t, 2, len(ret))
})
t.Run("ratio at threshold is not high", func(t *testing.T) {
// ratio > threshold is strict; ratio == threshold must NOT be treated as high.
const threshold = 0.85
a := newVolumeAllocator(allocConfig{
codeModes: codeModes,
allocatableSize: testConfig.AllocatableSize,
allocFactor: testConfig.AllocFactor,
allocatableDiskLoadThreshold: testConfig.AllocatableDiskLoadThreshold,
shardNum: testConfig.ShardNum,
diskUsageThreshold: threshold,
})
vols := generateVolume(mode, 4, 1)
for _, vol := range vols {
vol.volInfoBase.Status = proto.VolumeStatusIdle
a.idles[mode].addAllocatable(vol)
for _, unit := range vol.vUnits {
a.UpdateDiskHighUsage(unit.vuInfo.DiskID, threshold)
}
}
ret, _ := a.PreAlloc(ctx, mode, 2)
require.Equal(t, 2, len(ret))
})
t.Run("disk usage update overrides previous state", func(t *testing.T) {
// EC15P12: count = N+M+L-PutQuorum = 27-24 = 3 (tolerance).
// hasHighUsageDisk returns true only when > count (i.e. ≥ 4) disks are high
// (count<0 condition), meaning fewer than PutQuorum disks remain available.
const threshold = 0.85
const (
diskA = proto.DiskID(9001)
diskB = proto.DiskID(9002)
diskC = proto.DiskID(9003)
diskD = proto.DiskID(9004)
)
a := newVolumeAllocator(allocConfig{
codeModes: codeModes,
allocatableSize: testConfig.AllocatableSize,
allocFactor: testConfig.AllocFactor,
allocatableDiskLoadThreshold: testConfig.AllocatableDiskLoadThreshold,
shardNum: testConfig.ShardNum,
diskUsageThreshold: threshold,
})
units := []*volumeUnit{
{vuInfo: &clustermgr.VolumeUnitInfo{DiskID: diskA}},
{vuInfo: &clustermgr.VolumeUnitInfo{DiskID: diskB}},
{vuInfo: &clustermgr.VolumeUnitInfo{DiskID: diskC}},
{vuInfo: &clustermgr.VolumeUnitInfo{DiskID: diskD}},
}
// 3 high disks == count(3): count reaches 0 but not <0, still allowed.
a.UpdateDiskHighUsage(diskA, 0.9)
a.UpdateDiskHighUsage(diskB, 0.9)
a.UpdateDiskHighUsage(diskC, 0.9)
require.False(t, a.hasHighUsageDisk(units, mode))
// 4th disk also high → count goes negative → blocked.
a.UpdateDiskHighUsage(diskD, 0.9)
require.True(t, a.hasHighUsageDisk(units, mode))
// Drop diskA below threshold: only 3 of 4 remain high → unblocked.
a.UpdateDiskHighUsage(diskA, 0.3)
require.False(t, a.hasHighUsageDisk(units, mode))
})
}
func BenchmarkVolumeMgr_PreAllocVolume(b *testing.B) {
_, ctx := trace.StartSpanFromContext(context.Background(), "")
mode := codemode.EC15P12

View File

@ -192,6 +192,24 @@ func newTestWorkerTaskQueue(cancelPunishDuration, renewDuration time.Duration) *
}
}
// mockShardTask implements ShardTask for testing.
type mockShardTask struct {
source proto.ShardUnitInfoSimple
leader proto.ShardUnitInfoSimple
destination proto.ShardUnitInfoSimple
badDest proto.ShardUnitInfoSimple
}
func (t *mockShardTask) GetSource() proto.ShardUnitInfoSimple { return t.source }
func (t *mockShardTask) GetLeader() proto.ShardUnitInfoSimple { return t.leader }
func (t *mockShardTask) GetDestination() proto.ShardUnitInfoSimple { return t.destination }
func (t *mockShardTask) SetDestination(d proto.ShardUnitInfoSimple) {
t.destination = d
}
func (t *mockShardTask) SetLeader(l proto.ShardUnitInfoSimple) { t.leader = l }
func (t *mockShardTask) GetBadDestination() proto.ShardUnitInfoSimple { return t.badDest }
func (t *mockShardTask) ToTask() (*proto.Task, error) { return nil, nil }
func TestWorkerTaskQueue(t *testing.T) {
taskID1 := "task_id1"
idc := "z0"
@ -272,3 +290,190 @@ func TestWorkerTaskQueue(t *testing.T) {
_, err = wq.Complete(idc, taskID2, vunits([]proto.Vuid{4, 5, 6}), vunit(4))
require.EqualError(t, err, ErrUnmatchedVuids.Error())
}
func TestWorkerTaskQueueMissingPaths(t *testing.T) {
idc := "z0"
noSuchIdc := "z99"
cancelPunishDuration := 100 * time.Millisecond
renewDuration := 200 * time.Millisecond
taskID := "task_miss_1"
task := mockWorkerTask{src: vunits([]proto.Vuid{1, 2, 3}), dst: vunit(4)}
wq := newTestWorkerTaskQueue(cancelPunishDuration, renewDuration)
wq.AddPreparedTask(idc, taskID, &task)
// Acquire task to put it into doing state
_, _, exist := wq.Acquire(idc)
require.True(t, exist)
// Update: success
updatedTask := mockWorkerTask{src: vunits([]proto.Vuid{1, 2, 3}), dst: vunit(99)}
err := wq.Update(idc, taskID, &updatedTask)
require.NoError(t, err)
// Update: no such IDC
err = wq.Update(noSuchIdc, taskID, &updatedTask)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Query: success
wt, err := wq.Query(idc, taskID)
require.NoError(t, err)
require.Equal(t, vunit(99), wt.GetDestination())
// Query: no such IDC
_, err = wq.Query(noSuchIdc, taskID)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// SetLeaseExpiredS
wq.SetLeaseExpiredS(500 * time.Millisecond)
require.Equal(t, 500*time.Millisecond, wq.leaseExpiredS)
// Cancel: no such IDC
err = wq.Cancel(noSuchIdc, taskID, task.GetSources(), task.GetDestination())
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Reclaim: no such IDC
err = wq.Reclaim(noSuchIdc, taskID, task.GetSources(), task.GetDestination(), vunit(5), 0)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Renewal: no such IDC
err = wq.Renewal(noSuchIdc, taskID)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Complete: no such IDC
_, err = wq.Complete(noSuchIdc, taskID, task.GetSources(), task.GetDestination())
require.EqualError(t, err, errNoSuchIDCQueue.Error())
}
func TestQueueUpdateNotDoing(t *testing.T) {
q := NewQueue(200 * time.Millisecond)
msgID := "msg_todo"
require.NoError(t, q.Push(msgID, "data"))
// Update on a task in todo state (not doing) should fail
err := q.Update(msgID, "new_data")
require.EqualError(t, err, errTaskStateNotDoing.Error())
}
func TestShardTaskQueue(t *testing.T) {
idc := "z0"
noSuchIdc := "z99"
cancelPunish := 100 * time.Millisecond
src := proto.ShardUnitInfoSimple{Suid: proto.EncodeSuid(1, 0, 1), DiskID: 1}
dst := proto.ShardUnitInfoSimple{Suid: proto.EncodeSuid(1, 1, 1), DiskID: 2}
newDst := proto.ShardUnitInfoSimple{Suid: proto.EncodeSuid(1, 1, 2), DiskID: 3}
leader := proto.ShardUnitInfoSimple{Suid: proto.EncodeSuid(1, 2, 1), DiskID: 4}
taskID := "shard_task_1"
task := &mockShardTask{source: src, destination: dst, leader: leader}
q := NewShardTaskQueue(cancelPunish)
// AddPreparedTask: first add creates idc queue
q.AddPreparedTask(idc, taskID, task)
// AddPreparedTask: panic on duplicate
require.Panics(t, func() {
q.AddPreparedTask(idc, taskID, task)
})
// Acquire: no such IDC returns false
_, _, exist := q.Acquire(noSuchIdc)
require.False(t, exist)
// Acquire: success
id, wt, exist := q.Acquire(idc)
require.True(t, exist)
require.Equal(t, taskID, id)
require.Equal(t, src, wt.GetSource())
require.Equal(t, dst, wt.GetDestination())
// Acquire: queue empty (task is in doing)
_, _, exist = q.Acquire(idc)
require.False(t, exist)
// StatsTasks
todo, doing := q.StatsTasks()
require.Equal(t, 0, todo)
require.Equal(t, 1, doing)
// Cancel: no such IDC
err := q.Cancel(noSuchIdc, taskID, src, dst)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Cancel: unmatched suid
err = q.Cancel(idc, taskID, src, proto.ShardUnitInfoSimple{DiskID: 99})
require.EqualError(t, err, ErrUnmatchedSuid.Error())
// Cancel: success
err = q.Cancel(idc, taskID, src, dst)
require.NoError(t, err)
// wait cancel punish then re-acquire
time.Sleep(cancelPunish + 10*time.Millisecond)
_, _, exist = q.Acquire(idc)
require.True(t, exist)
// Reclaim: no such IDC
err = q.Reclaim(noSuchIdc, taskID, src, dst, newDst, newDst.DiskID)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Reclaim: unmatched suid
err = q.Reclaim(idc, taskID, src, proto.ShardUnitInfoSimple{DiskID: 99}, newDst, newDst.DiskID)
require.EqualError(t, err, ErrUnmatchedSuid.Error())
// Reclaim: success (updates destination)
err = q.Reclaim(idc, taskID, src, dst, newDst, newDst.DiskID)
require.NoError(t, err)
// re-acquire after reclaim (requeued immediately)
_, wt, exist = q.Acquire(idc)
require.True(t, exist)
require.Equal(t, newDst, wt.GetDestination())
// Update: no such IDC
_, err = q.Update(noSuchIdc, taskID, leader)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Update: success (updates leader)
newLeader := proto.ShardUnitInfoSimple{Suid: proto.EncodeSuid(1, 2, 2), DiskID: 5}
wt2, err := q.Update(idc, taskID, newLeader)
require.NoError(t, err)
require.Equal(t, newLeader, wt2.GetLeader())
// Query: no such IDC
_, err = q.Query(noSuchIdc, taskID)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Query: success
wt3, err := q.Query(idc, taskID)
require.NoError(t, err)
require.Equal(t, newDst, wt3.GetDestination())
// Renewal: no such IDC
err = q.Renewal(noSuchIdc, taskID)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Renewal: success
err = q.Renewal(idc, taskID)
require.NoError(t, err)
// Complete: no such IDC
_, err = q.Complete(noSuchIdc, taskID, src, newDst)
require.EqualError(t, err, errNoSuchIDCQueue.Error())
// Complete: unmatched suid
_, err = q.Complete(idc, taskID, src, proto.ShardUnitInfoSimple{DiskID: 99})
require.EqualError(t, err, ErrUnmatchedSuid.Error())
// Complete: success
completed, err := q.Complete(idc, taskID, src, newDst)
require.NoError(t, err)
require.Equal(t, newDst, completed.GetDestination())
// StatsTasks after complete: empty
todo, doing = q.StatsTasks()
require.Equal(t, 0, todo)
require.Equal(t, 0, doing)
}

View File

@ -118,3 +118,40 @@ func TestAbnormalReport(t *testing.T) {
require.True(t, rp.IsVuidReported(vuid))
require.False(t, rp.IsVuidReported(vuid+1))
}
func TestTaskStatsMgr_MissingPaths(t *testing.T) {
mgr := NewTaskStatsMgr(1, proto.TaskTypeDiskRepair)
// QueryTaskDetail: not found
_, err := mgr.QueryTaskDetail("nonexistent_task")
require.Error(t, err)
// ReportWorkerTaskStats: first call creates entry with StartTime
mgr.ReportWorkerTaskStats("task_progress", proto.TaskStatistics{Progress: 50}, 5, 5)
info, err := mgr.QueryTaskDetail("task_progress")
require.NoError(t, err)
require.False(t, info.Completed)
// ReportWorkerTaskStats: progress >= 100 sets CompleteTime and Completed=true
mgr.ReportWorkerTaskStats("task_progress", proto.TaskStatistics{Progress: 100}, 5, 5)
info, err = mgr.QueryTaskDetail("task_progress")
require.NoError(t, err)
require.True(t, info.Completed)
require.False(t, info.CompleteTime.IsZero())
// Second call to NewTaskStatsMgr with same labels hits AlreadyRegisteredError path
mgr2 := NewTaskStatsMgr(1, proto.TaskTypeDiskRepair)
require.NotNil(t, mgr2)
// NewCounter: second call with same labels hits AlreadyRegisteredError path
c1 := NewCounter(2, "test_type", KindSuccess)
c2 := NewCounter(2, "test_type", KindSuccess)
require.NotNil(t, c1)
require.NotNil(t, c2)
// NewAbnormalReporter: second call with same labels hits AlreadyRegisteredError path
rp1 := NewAbnormalReporter(proto.ClusterID(3), "test_type2", ChunkMissMigrateAbnormal)
rp2 := NewAbnormalReporter(proto.ClusterID(3), "test_type2", ChunkMissMigrateAbnormal)
require.NotNil(t, rp1)
require.NotNil(t, rp2)
}

View File

@ -50,3 +50,13 @@ func (mr *MockAllocVunitMockRecorder) AllocVolumeUnit(arg0, arg1, arg2, arg3 int
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllocVolumeUnit", reflect.TypeOf((*MockAllocVunit)(nil).AllocVolumeUnit), arg0, arg1, arg2, arg3)
}
// mockAllocShardUnit is a simple manual mock for IAllocShardUnit.
type mockAllocShardUnit struct {
ret *client.AllocShardUnitInfo
err error
}
func (m *mockAllocShardUnit) AllocShardUnit(_ context.Context, _ proto.Suid, _ []proto.DiskID) (*client.AllocShardUnitInfo, error) {
return m.ret, m.err
}

View File

@ -95,3 +95,60 @@ func TestGenTaskID(t *testing.T) {
id := GenTaskID(prefix, proto.Vid(1))
require.True(t, strings.HasPrefix(id, prefix))
}
func TestSubSuids(t *testing.T) {
a := []proto.Suid{1, 2, 3, 4, 5}
b := []proto.Suid{1, 2, 3}
c := SubSuids(a, b)
require.Equal(t, []proto.Suid{4, 5}, c)
// b is empty
require.Equal(t, a, SubSuids(a, nil))
// a is empty
require.Nil(t, SubSuids(nil, b))
}
func TestShouldAllocShardUnitAndRedo(t *testing.T) {
require.True(t, ShouldAllocShardUnitAndRedo(errcode.CodeNewSuidNotMatch))
require.True(t, ShouldAllocShardUnitAndRedo(errcode.CodeCMGetShardFailed))
require.False(t, ShouldAllocShardUnitAndRedo(0))
require.False(t, ShouldAllocShardUnitAndRedo(999))
}
func TestAllocShardUnitSafe(t *testing.T) {
ctx := context.Background()
errMock := errors.New("alloc shard unit error")
// allocation failed
cli := &mockAllocShardUnit{err: errMock}
_, err := AllocShardUnitSafe(ctx, cli, proto.ShardUnitInfoSimple{DiskID: 1}, proto.ShardUnitInfoSimple{DiskID: 2}, nil)
require.True(t, errors.Is(err, errMock))
// success: newly allocated disk does not overlap with src/dest
newSuid := proto.EncodeSuid(1, 0, 1)
cli = &mockAllocShardUnit{
ret: &client.AllocShardUnitInfo{
ShardUnitInfoSimple: proto.ShardUnitInfoSimple{
Suid: newSuid,
DiskID: 10,
},
},
}
ret, err := AllocShardUnitSafe(ctx, cli, proto.ShardUnitInfoSimple{DiskID: 1}, proto.ShardUnitInfoSimple{DiskID: 2}, nil)
require.NoError(t, err)
require.Equal(t, proto.DiskID(10), ret.DiskID)
// panic: newly allocated disk is the same as src
cli = &mockAllocShardUnit{
ret: &client.AllocShardUnitInfo{
ShardUnitInfoSimple: proto.ShardUnitInfoSimple{
Suid: newSuid,
DiskID: 1,
},
},
}
require.Panics(t, func() {
AllocShardUnitSafe(ctx, cli, proto.ShardUnitInfoSimple{DiskID: 1}, proto.ShardUnitInfoSimple{DiskID: 2}, nil)
})
}

View File

@ -42,3 +42,22 @@ func TestVolTaskLocker(t *testing.T) {
err = mu.TryLock(ctx, 1)
require.NoError(t, err)
}
func TestShardTaskLockerInst(t *testing.T) {
ctx := context.Background()
// singleton: two calls return same instance
locker1 := ShardTaskLockerInst()
locker2 := ShardTaskLockerInst()
require.Equal(t, locker1, locker2)
// TryLock and Unlock
err := locker1.TryLock(ctx, 9999)
require.NoError(t, err)
err = locker1.TryLock(ctx, 9999)
require.EqualError(t, err, ErrVidTaskConflict.Error())
locker1.Unlock(ctx, 9999)
err = locker1.TryLock(ctx, 9999)
require.NoError(t, err)
locker1.Unlock(ctx, 9999)
}

View File

@ -673,7 +673,7 @@ func (mgr *BlobDeleteMgr) deleteShard(ctx context.Context, location proto.VunitL
// has mark delete or delete before and just return
if (stageMgr.hasDelete(location.Vuid)) || (markDelete && stageMgr.hasMarkDel(location.Vuid)) {
span.Warnf("already delete and return: bid[%d], location[%+v], markDelete[%+v]",
span.Debugf("already delete and return: bid[%d], location[%+v], markDelete[%+v]",
bid, location, markDelete)
return nil
}

View File

@ -41,4 +41,8 @@ func TestBlobnode(t *testing.T) {
err = cli.Delete(ctx, proto.VunitLocation{}, proto.BlobID(1))
require.NoError(t, err)
client.EXPECT().RepairShard(any, any, any).Return(nil)
err = cli.RepairShard(ctx, "127.0.0.1:xxx", proto.ShardRepairTask{})
require.NoError(t, err)
}

View File

@ -31,6 +31,46 @@ import (
_ "github.com/cubefs/cubefs/blobstore/testing/nolog"
)
func TestValidMigrateTask(t *testing.T) {
taskID := GenMigrateTaskID(proto.TaskTypeDiskRepair, proto.DiskID(1), 1)
require.True(t, ValidMigrateTask(proto.TaskTypeDiskRepair, taskID))
require.False(t, ValidMigrateTask(proto.TaskTypeBalance, taskID))
}
func TestDiskInfoSimpleIsRepaired(t *testing.T) {
disk := &DiskInfoSimple{Status: proto.DiskStatusRepaired}
require.True(t, disk.IsRepaired())
disk.Status = proto.DiskStatusNormal
require.False(t, disk.IsRepaired())
}
func TestShardNodeDiskInfoMethods(t *testing.T) {
disk := &ShardNodeDiskInfo{}
disk.Status = proto.DiskStatusNormal
require.True(t, disk.IsHealth())
require.False(t, disk.IsBroken())
require.False(t, disk.IsDropped())
require.False(t, disk.IsRepaired())
require.True(t, disk.CanDropped())
disk.Status = proto.DiskStatusBroken
require.False(t, disk.IsHealth())
require.True(t, disk.IsBroken())
require.False(t, disk.CanDropped())
disk.Status = proto.DiskStatusDropped
require.True(t, disk.IsDropped())
require.True(t, disk.CanDropped())
disk.Status = proto.DiskStatusRepaired
require.True(t, disk.IsRepaired())
require.True(t, disk.CanDropped())
disk.Status = proto.DiskStatusRepairing
require.False(t, disk.CanDropped())
}
var (
defaultVolumeListMarker = proto.Vid(0)
defaultDiskListMarker = proto.DiskID(0)
@ -552,3 +592,216 @@ func TestClustermgrClient(t *testing.T) {
require.NoError(t, err)
}
}
func TestClustermgrClientExtra(t *testing.T) {
cli := NewClusterMgrClient(&cmapi.Config{}).(*clustermgrClient)
mockCli := NewMockClusterManager(gomock.NewController(t))
cli.client = mockCli
ctx := context.Background()
any := gomock.Any()
errMock := errors.New("fake error")
{
// set config
mockCli.EXPECT().SetConfig(any, any, any).Return(nil)
err := cli.SetConfig(ctx, "key", "value")
require.NoError(t, err)
mockCli.EXPECT().SetConfig(any, any, any).Return(errMock)
err = cli.SetConfig(ctx, "key", "value")
require.True(t, errors.Is(err, errMock))
}
{
// get service - matching clusterID
mockCli.EXPECT().GetService(any, any).Return(cmapi.ServiceInfo{
Nodes: []cmapi.ServiceNode{
{ClusterID: 1, Host: "127.0.0.1:8080"},
{ClusterID: 2, Host: "127.0.0.2:8080"},
},
}, nil)
hosts, err := cli.GetService(ctx, "scheduler", proto.ClusterID(1))
require.NoError(t, err)
require.Equal(t, 1, len(hosts))
require.Equal(t, "127.0.0.1:8080", hosts[0])
mockCli.EXPECT().GetService(any, any).Return(cmapi.ServiceInfo{}, errMock)
_, err = cli.GetService(ctx, "scheduler", proto.ClusterID(1))
require.True(t, errors.Is(err, errMock))
}
{
// list migrate tasks (public wrapper)
task1 := &proto.MigrateTask{TaskID: GenMigrateTaskID(proto.TaskTypeDiskRepair, proto.DiskID(1), 1), TaskType: proto.TaskTypeDiskRepair}
task1Bytes, _ := json.Marshal(task1)
opts := &cmapi.ListKvOpts{Prefix: GenMigrateTaskPrefix(proto.TaskTypeDiskRepair), Count: 10}
mockCli.EXPECT().ListKV(any, any).Return(cmapi.ListKvRet{
Kvs: []*cmapi.KeyValue{{Key: task1.TaskID, Value: task1Bytes}}, Marker: "",
}, nil)
tasks, marker, err := cli.ListMigrateTasks(ctx, proto.TaskTypeDiskRepair, opts)
require.NoError(t, err)
require.Equal(t, 1, len(tasks))
require.Equal(t, "", marker)
}
{
// get migrate task - task type mismatch
task1, _ := (&proto.MigrateTask{
TaskID: GenMigrateTaskID(proto.TaskTypeDiskRepair, proto.DiskID(1), 1),
TaskType: proto.TaskTypeDiskRepair,
}).ToTask()
taskBytes, _ := task1.Marshal()
mockCli.EXPECT().GetKV(any, any).Return(cmapi.GetKvRet{Value: taskBytes}, nil)
_, err := cli.GetMigrateTask(ctx, proto.TaskTypeBalance, task1.TaskID)
require.ErrorIs(t, err, errcode.ErrIllegalTaskType)
}
{
// get migrating disk - task type mismatch
diskMeta := &MigratingDiskMeta{Disk: &DiskInfoSimple{DiskID: proto.DiskID(1)}, TaskType: proto.TaskTypeDiskDrop}
diskMetaBytes, _ := json.Marshal(diskMeta)
mockCli.EXPECT().GetKV(any, any).Return(cmapi.GetKvRet{Value: diskMetaBytes}, nil)
_, err := cli.GetMigratingDisk(ctx, proto.TaskTypeBalance, proto.DiskID(1))
require.ErrorIs(t, err, errcode.ErrIllegalTaskType)
// get migrating disk - disk ID mismatch
diskMeta2 := &MigratingDiskMeta{Disk: &DiskInfoSimple{DiskID: proto.DiskID(99)}, TaskType: proto.TaskTypeDiskDrop}
diskMeta2Bytes, _ := json.Marshal(diskMeta2)
mockCli.EXPECT().GetKV(any, any).Return(cmapi.GetKvRet{Value: diskMeta2Bytes}, nil)
_, err = cli.GetMigratingDisk(ctx, proto.TaskTypeDiskDrop, proto.DiskID(1))
require.ErrorIs(t, err, errcode.ErrIllegalTaskType)
}
}
func TestClustermgrClientShardMethods(t *testing.T) {
cli := NewClusterMgrClient(&cmapi.Config{}).(*clustermgrClient)
mockCli := NewMockClusterManager(gomock.NewController(t))
cli.client = mockCli
ctx := context.Background()
any := gomock.Any()
errMock := errors.New("fake error")
{
// get shard info - error
mockCli.EXPECT().GetShardInfo(any, any).Return(nil, errMock)
_, err := cli.GetShardInfo(ctx, proto.ShardID(1))
require.True(t, errors.Is(err, errMock))
// get shard info - success
shardInfo := &cmapi.Shard{
ShardID: proto.ShardID(10),
AppliedIndex: 100,
LeaderDiskID: proto.DiskID(2),
Units: []cmapi.ShardUnit{
{Suid: proto.Suid(1), DiskID: proto.DiskID(1), Host: "127.0.0.1:xxx"},
{Suid: proto.Suid(2), DiskID: proto.DiskID(2), Host: "127.0.0.2:xxx"},
},
}
mockCli.EXPECT().GetShardInfo(any, any).Return(shardInfo, nil)
ret, err := cli.GetShardInfo(ctx, proto.ShardID(10))
require.NoError(t, err)
require.Equal(t, proto.ShardID(10), ret.ShardID)
require.Equal(t, 2, len(ret.ShardUnitInfos))
}
{
// update shard - error
mockCli.EXPECT().UpdateShard(any, any).Return(errMock)
err := cli.UpdateShard(ctx, &UpdateShardArgs{NewSuid: proto.Suid(1), OldSuid: proto.Suid(2)})
require.True(t, errors.Is(err, errMock))
// update shard - success
mockCli.EXPECT().UpdateShard(any, any).Return(nil)
err = cli.UpdateShard(ctx, &UpdateShardArgs{NewSuid: proto.Suid(3), OldSuid: proto.Suid(2)})
require.NoError(t, err)
}
{
// alloc shard unit - error
mockCli.EXPECT().AllocShardUnit(any, any).Return(nil, errMock)
_, err := cli.AllocShardUnit(ctx, proto.Suid(1), nil)
require.True(t, errors.Is(err, errMock))
// alloc shard unit - success
allocRet := &cmapi.AllocShardUnitRet{Suid: proto.Suid(5), DiskID: proto.DiskID(3), Host: "127.0.0.3:xxx"}
mockCli.EXPECT().AllocShardUnit(any, any).Return(allocRet, nil)
info, err := cli.AllocShardUnit(ctx, proto.Suid(1), nil)
require.NoError(t, err)
require.Equal(t, proto.Suid(5), info.Suid)
require.True(t, info.Learner)
}
{
// list disk shard units - error
mockCli.EXPECT().ListShardUnit(any, any).Return(nil, errMock)
_, err := cli.ListDiskShardUnits(ctx, proto.DiskID(1))
require.True(t, errors.Is(err, errMock))
// list disk shard units - success
units := []cmapi.ShardUnitInfo{
{Suid: proto.Suid(1), DiskID: proto.DiskID(1), Host: "host1", AppliedIndex: 10},
}
mockCli.EXPECT().ListShardUnit(any, any).Return(units, nil)
ret, err := cli.ListDiskShardUnits(ctx, proto.DiskID(1))
require.NoError(t, err)
require.Equal(t, 1, len(ret))
require.Equal(t, proto.Suid(1), ret[0].Suid)
}
{
// list shard - stub, always returns nil
shards, _, err := cli.ListShard(ctx, proto.ShardID(0), 10)
require.NoError(t, err)
require.Nil(t, shards)
}
{
// list shard disk (normal status)
disk1 := &cmapi.ShardNodeDiskInfo{
DiskInfo: cmapi.DiskInfo{Status: proto.DiskStatusNormal},
ShardNodeDiskHeartbeatInfo: cmapi.ShardNodeDiskHeartbeatInfo{DiskID: proto.DiskID(1)},
}
mockCli.EXPECT().ListShardNodeDisk(any, any).Return(cmapi.ListShardNodeDiskRet{
Disks: []*cmapi.ShardNodeDiskInfo{disk1}, Marker: defaultListDiskMarker,
}, nil)
disks, err := cli.ListShardDisk(ctx)
require.NoError(t, err)
require.Equal(t, 1, len(disks))
// list broken shard disk - error
mockCli.EXPECT().ListShardNodeDisk(any, any).Return(cmapi.ListShardNodeDiskRet{}, errMock)
_, err = cli.ListBrokenShardDisk(ctx)
require.True(t, errors.Is(err, errMock))
// list repairing shard disk - success
mockCli.EXPECT().ListShardNodeDisk(any, any).Return(cmapi.ListShardNodeDiskRet{
Disks: []*cmapi.ShardNodeDiskInfo{disk1}, Marker: defaultListDiskMarker,
}, nil)
disks, err = cli.ListRepairingShardDisk(ctx)
require.NoError(t, err)
require.Equal(t, 1, len(disks))
}
{
// set shard disk repairing
mockCli.EXPECT().SetShardNodeDisk(any, any, any).Return(nil)
err := cli.SetShardDiskRepairing(ctx, proto.DiskID(1))
require.NoError(t, err)
// set shard disk repaired
mockCli.EXPECT().SetShardNodeDisk(any, any, any).Return(nil)
err = cli.SetShardDiskRepaired(ctx, proto.DiskID(1))
require.NoError(t, err)
}
{
// get shard disk info - error
mockCli.EXPECT().ShardNodeDiskInfo(any, any).Return(nil, errMock)
_, err := cli.GetShardDiskInfo(ctx, proto.DiskID(1))
require.True(t, errors.Is(err, errMock))
// get shard disk info - success, also covers ShardNodeDiskInfo.set
info := &cmapi.ShardNodeDiskInfo{
DiskInfo: cmapi.DiskInfo{Status: proto.DiskStatusNormal, Host: "127.0.0.1:xxx"},
ShardNodeDiskHeartbeatInfo: cmapi.ShardNodeDiskHeartbeatInfo{
DiskID: proto.DiskID(1), FreeShardCnt: 10, UsedShardCnt: 5,
},
}
mockCli.EXPECT().ShardNodeDiskInfo(any, any).Return(info, nil)
disk, err := cli.GetShardDiskInfo(ctx, proto.DiskID(1))
require.NoError(t, err)
require.Equal(t, proto.DiskID(1), disk.DiskID)
require.True(t, disk.IsHealth())
}
}

View File

@ -31,6 +31,29 @@ import (
"github.com/cubefs/cubefs/blobstore/testing/mocks"
)
// newShardMigrateMgrWithCallback creates a manager with loadTaskCallback set.
func newShardMigrateMgrWithCallback(t *testing.T, cb loadTaskCallback) *ShardMigrateMgr {
ctr := gomock.NewController(t)
clusterMgr := NewMockClusterMgrAPI(ctr)
taskSwitch := mocks.NewMockSwitcher(ctr)
conf := &ShardMigrateConfig{
ClusterID: 0,
TaskCommonConfig: base.TaskCommonConfig{
PrepareQueueRetryDelayS: 0,
FinishQueueRetryDelayS: 0,
CancelPunishDurationS: 0,
WorkQueueSize: 3,
},
loadTaskCallback: cb,
}
mgr := NewShardMigrateMgr(clusterMgr, taskSwitch, conf, proto.TaskTypeShardDiskRepair)
m, ok := mgr.(*ShardMigrateMgr)
require.True(t, ok)
return m
}
var MockMigrateShardInfoMap = map[proto.ShardID]*client.ShardInfoSimple{
100: MockGenShardInfo(100, 0),
101: MockGenShardInfo(101, 0),
@ -468,4 +491,221 @@ func TestShardMigrateMgr_QueryTask(t *testing.T) {
_, err := mgr.GetTask(context.Background(), t1.TaskID)
require.True(t, errors.Is(err, errMock))
}
{
// unmarshal error: GetMigrateTask returns task with invalid data
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().GetMigrateTask(any, any, any).Return(&proto.Task{Data: []byte("invalid-json")}, nil)
_, err := mgr.GetTask(context.Background(), t1.TaskID)
require.Error(t, err)
}
}
func TestShardMigrateMgr_SimpleOps(t *testing.T) {
ctx := context.Background()
mgr := newShardMigrateMgr(t)
// ReportTask
err := mgr.ReportTask(ctx, nil)
require.NoError(t, err)
// StatQueueTaskCnt / Stats with empty queues
preparing, doing, finishing := mgr.StatQueueTaskCnt()
require.Equal(t, 0, preparing)
require.Equal(t, 0, doing)
require.Equal(t, 0, finishing)
stats := mgr.Stats()
require.Equal(t, 0, stats.PreparingCnt)
require.Equal(t, 0, stats.WorkerDoingCnt)
require.Equal(t, 0, stats.FinishingCnt)
// StatQueueTaskCnt with tasks in queues
t1 := mockGenShardMigrateTask(100, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStateInited, MockMigrateShardInfoMap)
mgr.prepareQueue.PushTask(t1.TaskID, t1)
t2 := mockGenShardMigrateTask(101, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStatePrepared, MockMigrateShardInfoMap)
mgr.workQueue.AddPreparedTask("z0", t2.TaskID, t2)
t3 := mockGenShardMigrateTask(102, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStateWorkCompleted, MockMigrateShardInfoMap)
mgr.finishQueue.PushTask(t3.TaskID, t3)
preparing, doing, finishing = mgr.StatQueueTaskCnt()
require.Greater(t, preparing, 0)
require.Greater(t, doing, 0)
require.Greater(t, finishing, 0)
// Enabled
mgr.taskSwitch.(*mocks.MockSwitcher).EXPECT().Enabled().Return(true)
require.True(t, mgr.Enabled())
// WaitEnable
mgr.taskSwitch.(*mocks.MockSwitcher).EXPECT().WaitEnable().Return()
mgr.WaitEnable()
// Done + Close
done := mgr.Done()
require.NotNil(t, done)
mgr.Close()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("Close should have closed Done channel")
}
}
func TestShardMigrateMgr_QueryTaskFull(t *testing.T) {
ctx := context.Background()
mgr := newShardMigrateMgr(t)
t1 := mockGenShardMigrateTask(100, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStatePrepared, MockMigrateShardInfoMap)
t2, err := t1.ToTask()
require.NoError(t, err)
// success
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().GetMigrateTask(any, any, any).Return(t2, nil)
ret, err := mgr.QueryTask(ctx, t1.TaskID)
require.NoError(t, err)
require.NotNil(t, ret)
require.Equal(t, t1.TaskType, ret.TaskType)
// GetTask error
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().GetMigrateTask(any, any, any).Return(nil, errMock)
_, err = mgr.QueryTask(ctx, t1.TaskID)
require.True(t, errors.Is(err, errMock))
}
func TestShardMigrateSuids(t *testing.T) {
ctx := context.Background()
// ListMigratingSuid
{
mgr := newShardMigrateMgr(t)
// error
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListAllMigrateTasksByDiskID(any, any, any).Return(nil, errMock)
_, err := mgr.ListMigratingSuid(ctx, 1)
require.True(t, errors.Is(err, errMock))
// success with task
t1 := mockGenShardMigrateTask(100, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStatePrepared, MockMigrateShardInfoMap)
protoTask, err := t1.ToTask()
require.NoError(t, err)
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListAllMigrateTasksByDiskID(any, any, any).Return([]*proto.Task{protoTask}, nil)
suids, err := mgr.ListMigratingSuid(ctx, 4)
require.NoError(t, err)
require.Len(t, suids, 1)
require.Equal(t, t1.Source.Suid, suids[0])
// success with empty list
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListAllMigrateTasksByDiskID(any, any, any).Return([]*proto.Task{}, nil)
suids, err = mgr.ListMigratingSuid(ctx, 4)
require.NoError(t, err)
require.Len(t, suids, 0)
}
// ListImmigratedSuid
{
mgr := newShardMigrateMgr(t)
// error
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListDiskShardUnits(any, any).Return(nil, errMock)
_, err := mgr.ListImmigratedSuid(ctx, 1)
require.True(t, errors.Is(err, errMock))
// success with shard units
suid := proto.EncodeSuid(100, 0, 1)
sunits := []*client.ShardUnitInfoSimple{
{Suid: suid, DiskID: 1},
}
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListDiskShardUnits(any, any).Return(sunits, nil)
suids, err := mgr.ListImmigratedSuid(ctx, 1)
require.NoError(t, err)
require.Len(t, suids, 1)
require.Equal(t, suid, suids[0])
// success with empty list
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListDiskShardUnits(any, any).Return([]*client.ShardUnitInfoSimple{}, nil)
suids, err = mgr.ListImmigratedSuid(ctx, 1)
require.NoError(t, err)
require.Len(t, suids, 0)
}
}
func TestMigratingShardDisks(t *testing.T) {
m := newMigratingShardDisks()
require.NotNil(t, m)
diskInfo1 := &client.ShardNodeDiskInfo{}
diskInfo2 := &client.ShardNodeDiskInfo{}
diskID1 := proto.DiskID(1)
diskID2 := proto.DiskID(2)
require.Equal(t, 0, m.size())
// add
m.add(diskID1, diskInfo1)
m.add(diskID2, diskInfo2)
require.Equal(t, 2, m.size())
// get existing
got, exist := m.get(diskID1)
require.True(t, exist)
require.Equal(t, diskInfo1, got)
// get non-existent
_, exist = m.get(proto.DiskID(999))
require.False(t, exist)
// list
list := m.list()
require.Len(t, list, 2)
// delete
m.delete(diskID1)
require.Equal(t, 1, m.size())
_, exist = m.get(diskID1)
require.False(t, exist)
}
func TestShardMigrateLoad_WithCallback(t *testing.T) {
callbackDiskIDs := make([]proto.DiskID, 0)
cb := func(ctx context.Context, diskID proto.DiskID) {
callbackDiskIDs = append(callbackDiskIDs, diskID)
}
mgr := newShardMigrateMgrWithCallback(t, cb)
t1 := mockGenShardMigrateTask(100, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStateInited, MockMigrateShardInfoMap)
protoTask, _ := t1.ToTask()
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().ListAllMigrateTasks(any, any).Return([]*proto.Task{protoTask}, nil)
err := mgr.Load()
require.NoError(t, err)
require.Len(t, callbackDiskIDs, 1)
require.Equal(t, t1.Source.DiskID, callbackDiskIDs[0])
}
func TestDealCancelReason_GetShardInfoError(t *testing.T) {
ctx := context.Background()
idc := "z0"
mgr := newShardMigrateMgr(t)
t1 := mockGenShardMigrateTask(100, proto.TaskTypeShardDiskRepair, "z0", 4, proto.ShardTaskStatePrepared, MockMigrateShardInfoMap)
mgr.workQueue.AddPreparedTask(idc, t1.TaskID, t1)
// code triggers GetShardInfo, but GetShardInfo fails → early return, no panic
mgr.clusterMgrCli.(*MockClusterMgrAPI).EXPECT().GetShardInfo(any, any).Return(nil, errMock)
taskArgs := genShardTaskArgs(t1, errcode.ErrShardNodeNotLeader.Error(), errcode.CodeShardNodeNotLeader)
err := mgr.CancelTask(ctx, taskArgs)
require.NoError(t, err)
}
func TestShardMigrateRun_QueueFull(t *testing.T) {
mgr := newShardMigrateMgr(t)
mgr.taskSwitch.(*mocks.MockSwitcher).EXPECT().WaitEnable().AnyTimes().Return()
mgr.taskSwitch.(*mocks.MockSwitcher).EXPECT().Enabled().AnyTimes().Return(true)
// fill workQueue to capacity (WorkQueueSize=3) to trigger the "queue full" branch
for i := proto.ShardID(200); i < 203; i++ {
MockMigrateShardInfoMap[i] = MockGenShardInfo(i, 0)
task := mockGenShardMigrateTask(i, proto.TaskTypeShardDiskRepair, "z0", proto.DiskID(i), proto.ShardTaskStatePrepared, MockMigrateShardInfoMap)
mgr.workQueue.AddPreparedTask("z0", task.TaskID, task)
}
mgr.Run()
// give the loop time to hit the "queue full" branch
time.Sleep(5 * time.Millisecond)
}