Skip to content

Commit 4f768f6

Browse files
committed
fix: preserve backend volume associations during reconciliation
1 parent aea926a commit 4f768f6

2 files changed

Lines changed: 103 additions & 24 deletions

File tree

core/concurrent_core.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1725,6 +1725,15 @@ func (o *ConcurrentTridentOrchestrator) upsertBackend(
17251725
if err != nil {
17261726
return nil, err
17271727
}
1728+
// A replacement backend is created with an empty local volume map.
1729+
// Preserve the existing backend-to-volume associations before node
1730+
// access reconciliation and before replacing the cached backend.
1731+
volumeCount := copyBackendVolumes(originalBackend, backend)
1732+
Logc(ctx).WithFields(LogFields{
1733+
"backend": backend.Name(),
1734+
"backendUUID": backend.BackendUUID(),
1735+
"volumeCount": volumeCount,
1736+
}).Debug("Copied volume associations to replacement backend.")
17281737
}
17291738

17301739
// Node access rules may have changed in the backend config
@@ -9656,3 +9665,22 @@ func (o *ConcurrentTridentOrchestrator) resolveAndSetEffectiveAutogrowPolicy(
96569665

96579666
return effectiveAGPolicy, policyErr
96589667
}
9668+
9669+
func copyBackendVolumes(originalBackend, newBackend storage.Backend) int {
9670+
if originalBackend == nil || newBackend == nil {
9671+
return 0
9672+
}
9673+
9674+
if originalBackend.BackendUUID() != newBackend.BackendUUID() {
9675+
return 0
9676+
}
9677+
9678+
count := 0
9679+
originalBackend.Volumes().Range(func(key, value interface{}) bool {
9680+
newBackend.Volumes().Store(key, value)
9681+
count++
9682+
return true
9683+
})
9684+
9685+
return count
9686+
}

core/concurrent_core_test.go

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
444472
func 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

Comments
 (0)