@@ -441,6 +441,34 @@ func getFakeVolume(name, backendUUID string) *storage.Volume {
441441 return fakeVolume
442442}
443443
444+ func countBackendVolumes(backend storage.Backend) int {
445+ if backend == nil {
446+ return 0
447+ }
448+
449+ count := 0
450+ backend.Volumes().Range(func(_, _ any) bool {
451+ count++
452+ return true
453+ })
454+
455+ return count
456+ }
457+
458+ func requireBackendVolume(
459+ t *testing.T, backend storage.Backend, volumeName, expectedBackendUUID string,
460+ ) {
461+ t.Helper()
462+
463+ value, found := backend.Volumes().Load(volumeName)
464+ require.Truef(t, found, "volume %q is absent from backend %q", volumeName, backend.Name())
465+
466+ volume, ok := value.(*storage.Volume)
467+ require.Truef(t, ok, "unexpected value type for volume %q: %T", volumeName, value)
468+ require.Equal(t, volumeName, volume.Config.Name)
469+ require.Equal(t, expectedBackendUUID, volume.BackendUUID)
470+ }
471+
444472func getFakeNode(name string) *models.Node {
445473 fakeNode := &models.Node{
446474 Name: name,
@@ -581,9 +609,7 @@ func TestBootstrapConcurrentCore(t *testing.T) {
581609 mockStoreClient.EXPECT().GetVolumePublications(gomock.Any()).Return(pubs, nil).AnyTimes()
582610 mockStoreClient.EXPECT().GetNodes(gomock.Any()).Return(nodes, nil).AnyTimes()
583611 mockStoreClient.EXPECT().IsBackendDeleting(gomock.Any(), gomock.Any()).Return(false).AnyTimes()
584- // Bootstrap backfills the node-name label on existing publications,
585- // which triggers an asynchronous UpdateVolumePublication; allow it.
586- mockStoreClient.EXPECT().UpdateVolumePublication(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
612+ mockStoreClient.EXPECT().GetVolumeMoves(gomock.Any()).Return(nil, nil).AnyTimes()
587613 },
588614 verifyError: func(err error) {
589615 assert.NoError(t, err)
@@ -682,9 +708,7 @@ func TestBootstrapConcurrentCore(t *testing.T) {
682708 mockStoreClient.EXPECT().GetVolumePublications(gomock.Any()).Return(pubs, nil).AnyTimes()
683709 mockStoreClient.EXPECT().GetNodes(gomock.Any()).Return(nodes, nil).AnyTimes()
684710 mockStoreClient.EXPECT().IsBackendDeleting(gomock.Any(), gomock.Any()).Return(false).AnyTimes()
685- // Bootstrap backfills the node-name label on existing publications,
686- // which triggers an asynchronous UpdateVolumePublication; allow it.
687- mockStoreClient.EXPECT().UpdateVolumePublication(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
711+ mockStoreClient.EXPECT().GetVolumeMoves(gomock.Any()).Return(nil, nil).AnyTimes()
688712 },
689713 verifyError: func(err error) {
690714 assert.NoError(t, err)
@@ -13708,12 +13732,12 @@ func TestPeriodicallyReconcileNodeAccessOnBackendsConcurrentCore(t *testing.T) {
1370813732
1370913733 // Start loop
1371013734 go o.PeriodicallyReconcileNodeAccessOnBackends()
13711- for o.getStopNodeAccessLoop() == nil {
13735+ for o.stopNodeAccessLoop == nil {
1371213736 // Wait for loop to initialize
1371313737 time.Sleep(10 * time.Millisecond)
1371413738 }
1371513739
13716- assert.NotNil(t, o.getStopNodeAccessLoop() , "loop channel should be initialized")
13740+ assert.NotNil(t, o.stopNodeAccessLoop , "loop channel should be initialized")
1371713741
1371813742 time.Sleep(500 * time.Millisecond) // Wait for loop to do some work
1371913743
@@ -13737,7 +13761,7 @@ func TestPeriodicallyReconcileBackendStateConcurrentCore(t *testing.T) {
1373713761 // No setup needed for zero interval test
1373813762 },
1373913763 verifyBehavior: func(t *testing.T, o *ConcurrentTridentOrchestrator) {
13740- assert.Nil(t, o.getStopReconcileBackendLoop() , "reconcile backend loop should not have started")
13764+ assert.Nil(t, o.stopReconcileBackendLoop , "reconcile backend loop should not have started")
1374113765 },
1374213766 },
1374313767 {
@@ -13758,7 +13782,7 @@ func TestPeriodicallyReconcileBackendStateConcurrentCore(t *testing.T) {
1375813782 },
1375913783 verifyBehavior: func(t *testing.T, o *ConcurrentTridentOrchestrator) {
1376013784 // The loop should be running (stopReconcileBackendLoop should not be nil)
13761- assert.NotNil(t, o.getStopReconcileBackendLoop() , "reconcile backend loop should have started")
13785+ assert.NotNil(t, o.stopReconcileBackendLoop , "reconcile backend loop should have started")
1376213786 },
1376313787 },
1376413788 {
@@ -13769,7 +13793,7 @@ func TestPeriodicallyReconcileBackendStateConcurrentCore(t *testing.T) {
1376913793 },
1377013794 verifyBehavior: func(t *testing.T, o *ConcurrentTridentOrchestrator) {
1377113795 // The loop should still be running even with no backends
13772- assert.NotNil(t, o.getStopReconcileBackendLoop() , "reconcile backend loop should have started")
13796+ assert.NotNil(t, o.stopReconcileBackendLoop , "reconcile backend loop should have started")
1377313797 },
1377413798 },
1377513799 {
@@ -13790,7 +13814,7 @@ func TestPeriodicallyReconcileBackendStateConcurrentCore(t *testing.T) {
1379013814 },
1379113815 verifyBehavior: func(t *testing.T, o *ConcurrentTridentOrchestrator) {
1379213816 // The loop should be running
13793- assert.NotNil(t, o.getStopReconcileBackendLoop() , "reconcile backend loop should have started")
13817+ assert.NotNil(t, o.stopReconcileBackendLoop , "reconcile backend loop should have started")
1379413818 },
1379513819 },
1379613820 }
@@ -13813,7 +13837,7 @@ func TestPeriodicallyReconcileBackendStateConcurrentCore(t *testing.T) {
1381313837
1381413838 go o.PeriodicallyReconcileBackendState(tt.pollInterval)
1381513839 if tt.pollInterval > 0 {
13816- for o.getStopReconcileBackendLoop() == nil {
13840+ for o.stopReconcileBackendLoop == nil {
1381713841 // Wait for loop to initialize
1381813842 time.Sleep(10 * time.Millisecond)
1381913843 }
@@ -13840,6 +13864,7 @@ func TestReconcileBackendStateConcurrentCore(t *testing.T) {
1384013864 backendUUID string
1384113865 setupMocks func(mockCtrl *gomock.Controller, mockStoreClient *mockpersistentstore.MockStoreClient, o *ConcurrentTridentOrchestrator) storage.Backend
1384213866 verifyError func(t *testing.T, err error)
13867+ verifyState func(t *testing.T)
1384313868 }{
1384413869 {
1384513870 name: "BackendNotFound",
@@ -14006,7 +14031,7 @@ func TestReconcileBackendStateConcurrentCore(t *testing.T) {
1400614031 },
1400714032 },
1400814033 {
14009- name: "ReconcileBackendPoolsChange ",
14034+ name: "ReconcileBackendPoolsChangePreservesVolumes ",
1401014035 backendUUID: "uuid1",
1401114036 setupMocks: func(mockCtrl *gomock.Controller, mockStoreClient *mockpersistentstore.MockStoreClient, o *ConcurrentTridentOrchestrator) storage.Backend {
1401214037 mockBackend := getMockBackendWithMap(mockCtrl, []storage.Pool{fakePool1}, map[string]string{
@@ -14021,7 +14046,14 @@ func TestReconcileBackendStateConcurrentCore(t *testing.T) {
1402114046 fakeBackend := getFakeBackend("backend1", "uuid1", nil)
1402214047 fakeConfigBytes, _ := fakeBackend.MarshalDriverConfig()
1402314048
14024- // Setup backend expectations for offline backend
14049+ volume1 := getFakeVolume("volume1", "uuid1")
14050+ volume2 := getFakeVolume("volume2", "uuid1")
14051+ backendVolumes := makeSyncMapFromMap(map[string]*storage.Volume{
14052+ volume1.Config.Name: volume1,
14053+ volume2.Config.Name: volume2,
14054+ })
14055+
14056+ // Setup backend expectations.
1402514057 mockBackend.EXPECT().CanGetState().Return(true).Times(1)
1402614058 mockBackend.EXPECT().GetBackendState(gomock.Any()).Return("", bitset).Times(2)
1402714059 mockBackend.EXPECT().UpdateBackendState(gomock.Any(), "").Times(1)
@@ -14034,19 +14066,33 @@ func TestReconcileBackendStateConcurrentCore(t *testing.T) {
1403414066 mockBackend.EXPECT().State().Return(storage.Online).AnyTimes()
1403514067 mockBackend.EXPECT().Online().Return(true).AnyTimes()
1403614068 mockBackend.EXPECT().HasVolumes().Return(true).AnyTimes()
14069+ mockBackend.EXPECT().Volumes().Return(backendVolumes).AnyTimes()
1403714070 mockBackend.EXPECT().SmartCopy().Return(mockBackend).AnyTimes()
1403814071 mockBackend.EXPECT().ConstructPersistent(gomock.Any()).Return(&storage.BackendPersistent{Name: "backend1", BackendUUID: "uuid1"}).AnyTimes()
1403914072 mockBackend.EXPECT().Terminate(gomock.Any()).Times(1)
1404014073 mockStoreClient.EXPECT().UpdateBackend(gomock.Any(), gomock.Any()).Return(nil).Times(1)
1404114074
14042- // Add backend to cache
14075+ // Model the state created by bootstrap: both the global volume cache
14076+ // and the original backend-local volume map contain the volumes.
1404314077 addBackendsToCache(t, mockBackend)
14078+ addVolumesToCache(t, volume1, volume2)
1404414079
1404514080 return mockBackend
1404614081 },
1404714082 verifyError: func(t *testing.T, err error) {
1404814083 assert.NoError(t, err)
1404914084 },
14085+ verifyState: func(t *testing.T) {
14086+ replacement := getBackendByUuidFromCache(t, "uuid1")
14087+ require.NotNil(t, replacement)
14088+ require.Equal(t, 2, countBackendVolumes(replacement))
14089+ requireBackendVolume(t, replacement, "volume1", "uuid1")
14090+ requireBackendVolume(t, replacement, "volume2", "uuid1")
14091+
14092+ // The independent global volume cache must remain intact as well.
14093+ require.Equal(t, "uuid1", getVolumeByNameFromCache(t, "volume1").BackendUUID)
14094+ require.Equal(t, "uuid1", getVolumeByNameFromCache(t, "volume2").BackendUUID)
14095+ },
1405014096 },
1405114097 {
1405214098 name: "ReconcileBackendVersionChangeStoreError",
@@ -14110,6 +14156,9 @@ func TestReconcileBackendStateConcurrentCore(t *testing.T) {
1411014156 if tt.verifyError != nil {
1411114157 tt.verifyError(t, err)
1411214158 }
14159+ if tt.verifyState != nil {
14160+ tt.verifyState(t)
14161+ }
1411314162
1411414163 persistenceCleanup(t, o)
1411514164 })
@@ -23219,7 +23268,7 @@ func TestConcurrentTridentOrchestrator_StageVolumeMove(t *testing.T) {
2321923268 assert.Nil(t, cached.Config.MoveInfo, "backend stage failure must not set MoveInfo")
2322023269 })
2322123270
23222- t.Run("happy path sets MoveInfo on success ", func(t *testing.T) {
23271+ t.Run("happy path records MoveInfo", func(t *testing.T) {
2322323272 o, vol, mockBackend, mockStoreClient, _ := setupConcurrentMoveInfoFixture(t)
2322423273
2322523274 mockBackend.EXPECT().StageVolumeMove(gomock.Any(), gomock.Any(), gomock.Any()).
@@ -23232,7 +23281,7 @@ func TestConcurrentTridentOrchestrator_StageVolumeMove(t *testing.T) {
2323223281
2323323282 cached := getVolumeByNameFromCache(t, vol.Config.Name)
2323423283 require.NotNil(t, cached)
23235- require.NotNil(t, cached.Config.MoveInfo, "StageVolumeMove must set MoveInfo on success" )
23284+ require.NotNil(t, cached.Config.MoveInfo)
2323623285 assert.Equal(t, models.VolumeMoveStateControllerStaging, cached.Config.MoveInfo.State)
2323723286 })
2323823287}
@@ -23275,7 +23324,7 @@ func TestConcurrentTridentOrchestrator_MoveVolume(t *testing.T) {
2327523324 assert.True(t, errors.IsVolumeStateError(err))
2327623325 })
2327723326
23278- t.Run("backend transient error does not project MoveInfo", func(t *testing.T) {
23327+ t.Run("backend transient error still updates MoveInfo", func(t *testing.T) {
2327923328 o, vol, mockBackend, mockStoreClient, _ := setupConcurrentMoveInfoFixture(t)
2328023329 vol.Pool = "pool-original"
2328123330 addVolumesToCache(t, vol)
@@ -23289,11 +23338,12 @@ func TestConcurrentTridentOrchestrator_MoveVolume(t *testing.T) {
2328923338
2329023339 cached := getVolumeByNameFromCache(t, vol.Config.Name)
2329123340 require.NotNil(t, cached)
23292- assert.Nil(t, cached.Config.MoveInfo, "MoveVolume must not project MoveInfo; commitState owns projection")
23341+ require.NotNil(t, cached.Config.MoveInfo, "transient error must still update MoveInfo")
23342+ assert.Equal(t, models.VolumeMoveStateMoving, cached.Config.MoveInfo.State)
2329323343 assert.Equal(t, "pool-original", cached.Pool, "pool should not change on transient move errors")
2329423344 })
2329523345
23296- t.Run("happy path persists target pool and sets MoveInfo", func(t *testing.T) {
23346+ t.Run("happy path records MoveInfo", func(t *testing.T) {
2329723347 o, vol, mockBackend, mockStoreClient, _ := setupConcurrentMoveInfoFixture(t)
2329823348 vol.Pool = "pool-original"
2329923349 addVolumesToCache(t, vol)
@@ -23310,7 +23360,7 @@ func TestConcurrentTridentOrchestrator_MoveVolume(t *testing.T) {
2331023360 cached := getVolumeByNameFromCache(t, vol.Config.Name)
2331123361 require.NotNil(t, cached)
2331223362 require.NotNil(t, cached.Config.MoveInfo)
23313- assert.NotNil (t, cached.Config.MoveInfo, "MoveVolume must set MoveInfo on success" )
23363+ assert.Equal (t, models.VolumeMoveStateMoving, cached.Config.MoveInfo.State )
2331423364 assert.Equal(t, "pool-target", cached.Pool, "pool should be updated to move target on success")
2331523365 })
2331623366}
@@ -23360,7 +23410,7 @@ func TestConcurrentTridentOrchestrator_UnstageVolumeMove(t *testing.T) {
2336023410 "backend unstage failure must leave MoveInfo untouched")
2336123411 })
2336223412
23363- t.Run("happy path does not clear MoveInfo", func(t *testing.T) {
23413+ t.Run("happy path clears MoveInfo", func(t *testing.T) {
2336423414 o, vol, mockBackend, mockStoreClient, _ := setupConcurrentMoveInfoFixture(t)
2336523415
2336623416 vol.Config.MoveInfo = newConcMoveInfo(vol.Config.Name, models.VolumeMoveStateControllerUnstaging)
@@ -23376,7 +23426,8 @@ func TestConcurrentTridentOrchestrator_UnstageVolumeMove(t *testing.T) {
2337623426
2337723427 cached := getVolumeByNameFromCache(t, vol.Config.Name)
2337823428 require.NotNil(t, cached)
23379- assert.NotNil(t, cached.Config.MoveInfo, "UnstageVolumeMove must not clear MoveInfo")
23429+ assert.Nil(t, cached.Config.MoveInfo,
23430+ "successful unstage must clear MoveInfo")
2338023431 })
2338123432}
2338223433
0 commit comments